PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / 6.5.1.7
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin v6.5.1.7
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / lib / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Calculation / Calculation.php

Calculation.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin 6.5.1.7, at lib/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php

5,795 lines 243.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace PhpOffice\PhpSpreadsheet\Calculation;
4
5 use PhpOffice\PhpSpreadsheet\Calculation\Engine\BranchPruner;
6 use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack;
7 use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
8 use PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
9 use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack;
10 use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
11 use PhpOffice\PhpSpreadsheet\Cell\Cell;
12 use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
13 use PhpOffice\PhpSpreadsheet\Cell\DataType;
14 use PhpOffice\PhpSpreadsheet\DefinedName;
15 use PhpOffice\PhpSpreadsheet\ReferenceHelper;
16 use PhpOffice\PhpSpreadsheet\Shared;
17 use PhpOffice\PhpSpreadsheet\Spreadsheet;
18 use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
19 use ReflectionClassConstant;
20 use ReflectionMethod;
21 use ReflectionParameter;
22 use Throwable;
23
24 class Calculation
25 {
26 /** Constants */
27 /** Regular Expressions */
28 // Numeric operand
29 const CALCULATION_REGEXP_NUMBER = '[-+]?\d*\.?\d+(e[-+]?\d+)?';
30 // String operand
31 const CALCULATION_REGEXP_STRING = '"(?:[^"]|"")*"';
32 // Opening bracket
33 const CALCULATION_REGEXP_OPENBRACE = '\(';
34 // Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
35 const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
36 // Cell reference (cell or range of cells, with or without a sheet reference)
37 const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
38 // Cell reference (with or without a sheet reference) ensuring absolute/relative
39 const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])';
40 const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\".(?:[^\"]|\"[^!])?\"))!)?(\$?[a-z]{1,3})):(?![.*])';
41 const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])';
42 // Cell reference (with or without a sheet reference) ensuring absolute/relative
43 // Cell ranges ensuring absolute/relative
44 const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
45 const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})';
46 // Defined Names: Named Range of cells, or Named Formulae
47 const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)';
48 // Structured Reference (Fully Qualified and Unqualified)
49 const CALCULATION_REGEXP_STRUCTURED_REFERENCE = '([\p{L}_\\\][\p{L}\p{N}\._]+)?(\[(?:[^\d\]+-])?)';
50 // Error
51 const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?';
52
53 /** constants */
54 const RETURN_ARRAY_AS_ERROR = 'error';
55 const RETURN_ARRAY_AS_VALUE = 'value';
56 const RETURN_ARRAY_AS_ARRAY = 'array';
57
58 const FORMULA_OPEN_FUNCTION_BRACE = '(';
59 const FORMULA_CLOSE_FUNCTION_BRACE = ')';
60 const FORMULA_OPEN_MATRIX_BRACE = '{';
61 const FORMULA_CLOSE_MATRIX_BRACE = '}';
62 const FORMULA_STRING_QUOTE = '"';
63
64 /** @var string */
65 private static $returnArrayAsType = self::RETURN_ARRAY_AS_VALUE;
66
67 /**
68 * Instance of this class.
69 *
70 * @var ?Calculation
71 */
72 private static $instance;
73
74 /**
75 * Instance of the spreadsheet this Calculation Engine is using.
76 *
77 * @var ?Spreadsheet
78 */
79 private $spreadsheet;
80
81 /**
82 * Calculation cache.
83 *
84 * @var array
85 */
86 private $calculationCache = [];
87
88 /**
89 * Calculation cache enabled.
90 *
91 * @var bool
92 */
93 private $calculationCacheEnabled = true;
94
95 /**
96 * @var BranchPruner
97 */
98 private $branchPruner;
99
100 /**
101 * @var bool
102 */
103 private $branchPruningEnabled = true;
104
105 /**
106 * List of operators that can be used within formulae
107 * The true/false value indicates whether it is a binary operator or a unary operator.
108 */
109 private const CALCULATION_OPERATORS = [
110 '+' => true, '-' => true, '*' => true, '/' => true,
111 '^' => true, '&' => true, '%' => false, '~' => false,
112 '>' => true, '<' => true, '=' => true, '>=' => true,
113 '<=' => true, '<>' => true, '' => true, '' => true,
114 ':' => true,
115 ];
116
117 /**
118 * List of binary operators (those that expect two operands).
119 */
120 private const BINARY_OPERATORS = [
121 '+' => true, '-' => true, '*' => true, '/' => true,
122 '^' => true, '&' => true, '>' => true, '<' => true,
123 '=' => true, '>=' => true, '<=' => true, '<>' => true,
124 '' => true, '' => true, ':' => true,
125 ];
126
127 /**
128 * The debug log generated by the calculation engine.
129 *
130 * @var Logger
131 */
132 private $debugLog;
133
134 /**
135 * Flag to determine how formula errors should be handled
136 * If true, then a user error will be triggered
137 * If false, then an exception will be thrown.
138 *
139 * @var ?bool
140 *
141 * @deprecated 1.25.2 use setSuppressFormulaErrors() instead
142 */
143 public $suppressFormulaErrors;
144
145 /** @var bool */
146 private $suppressFormulaErrorsNew = false;
147
148 /**
149 * Error message for any error that was raised/thrown by the calculation engine.
150 *
151 * @var null|string
152 */
153 public $formulaError;
154
155 /**
156 * An array of the nested cell references accessed by the calculation engine, used for the debug log.
157 *
158 * @var CyclicReferenceStack
159 */
160 private $cyclicReferenceStack;
161
162 /** @var array */
163 private $cellStack = [];
164
165 /**
166 * Current iteration counter for cyclic formulae
167 * If the value is 0 (or less) then cyclic formulae will throw an exception,
168 * otherwise they will iterate to the limit defined here before returning a result.
169 *
170 * @var int
171 */
172 private $cyclicFormulaCounter = 1;
173
174 /** @var string */
175 private $cyclicFormulaCell = '';
176
177 /**
178 * Number of iterations for cyclic formulae.
179 *
180 * @var int
181 */
182 public $cyclicFormulaCount = 1;
183
184 /**
185 * The current locale setting.
186 *
187 * @var string
188 */
189 private static $localeLanguage = 'en_us'; // US English (default locale)
190
191 /**
192 * List of available locale settings
193 * Note that this is read for the locale subdirectory only when requested.
194 *
195 * @var string[]
196 */
197 private static $validLocaleLanguages = [
198 'en', // English (default language)
199 ];
200
201 /**
202 * Locale-specific argument separator for function arguments.
203 *
204 * @var string
205 */
206 private static $localeArgumentSeparator = ',';
207
208 /** @var array */
209 private static $localeFunctions = [];
210
211 /**
212 * Locale-specific translations for Excel constants (True, False and Null).
213 *
214 * @var array<string, string>
215 */
216 private static $localeBoolean = [
217 'TRUE' => 'TRUE',
218 'FALSE' => 'FALSE',
219 'NULL' => 'NULL',
220 ];
221
222 public static function getLocaleBoolean(string $index): string
223 {
224 return self::$localeBoolean[$index];
225 }
226
227 /**
228 * Excel constant string translations to their PHP equivalents
229 * Constant conversion from text name/value to actual (datatyped) value.
230 *
231 * @var array<string, mixed>
232 */
233 private static $excelConstants = [
234 'TRUE' => true,
235 'FALSE' => false,
236 'NULL' => null,
237 ];
238
239 public static function keyInExcelConstants(string $key): bool
240 {
241 return array_key_exists($key, self::$excelConstants);
242 }
243
244 /** @return mixed */
245 public static function getExcelConstants(string $key)
246 {
247 return self::$excelConstants[$key];
248 }
249
250 /**
251 * Array of functions usable on Spreadsheet.
252 * In theory, this could be const rather than static;
253 * however, Phpstan breaks trying to analyze it when attempted.
254 *
255 *@var array
256 */
257 private static $phpSpreadsheetFunctions = [
258 'ABS' => [
259 'category' => Category::CATEGORY_MATH_AND_TRIG,
260 'functionCall' => [MathTrig\Absolute::class, 'evaluate'],
261 'argumentCount' => '1',
262 ],
263 'ACCRINT' => [
264 'category' => Category::CATEGORY_FINANCIAL,
265 'functionCall' => [Financial\Securities\AccruedInterest::class, 'periodic'],
266 'argumentCount' => '4-8',
267 ],
268 'ACCRINTM' => [
269 'category' => Category::CATEGORY_FINANCIAL,
270 'functionCall' => [Financial\Securities\AccruedInterest::class, 'atMaturity'],
271 'argumentCount' => '3-5',
272 ],
273 'ACOS' => [
274 'category' => Category::CATEGORY_MATH_AND_TRIG,
275 'functionCall' => [MathTrig\Trig\Cosine::class, 'acos'],
276 'argumentCount' => '1',
277 ],
278 'ACOSH' => [
279 'category' => Category::CATEGORY_MATH_AND_TRIG,
280 'functionCall' => [MathTrig\Trig\Cosine::class, 'acosh'],
281 'argumentCount' => '1',
282 ],
283 'ACOT' => [
284 'category' => Category::CATEGORY_MATH_AND_TRIG,
285 'functionCall' => [MathTrig\Trig\Cotangent::class, 'acot'],
286 'argumentCount' => '1',
287 ],
288 'ACOTH' => [
289 'category' => Category::CATEGORY_MATH_AND_TRIG,
290 'functionCall' => [MathTrig\Trig\Cotangent::class, 'acoth'],
291 'argumentCount' => '1',
292 ],
293 'ADDRESS' => [
294 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
295 'functionCall' => [LookupRef\Address::class, 'cell'],
296 'argumentCount' => '2-5',
297 ],
298 'AGGREGATE' => [
299 'category' => Category::CATEGORY_MATH_AND_TRIG,
300 'functionCall' => [Functions::class, 'DUMMY'],
301 'argumentCount' => '3+',
302 ],
303 'AMORDEGRC' => [
304 'category' => Category::CATEGORY_FINANCIAL,
305 'functionCall' => [Financial\Amortization::class, 'AMORDEGRC'],
306 'argumentCount' => '6,7',
307 ],
308 'AMORLINC' => [
309 'category' => Category::CATEGORY_FINANCIAL,
310 'functionCall' => [Financial\Amortization::class, 'AMORLINC'],
311 'argumentCount' => '6,7',
312 ],
313 'ANCHORARRAY' => [
314 'category' => Category::CATEGORY_UNCATEGORISED,
315 'functionCall' => [Functions::class, 'DUMMY'],
316 'argumentCount' => '*',
317 ],
318 'AND' => [
319 'category' => Category::CATEGORY_LOGICAL,
320 'functionCall' => [Logical\Operations::class, 'logicalAnd'],
321 'argumentCount' => '1+',
322 ],
323 'ARABIC' => [
324 'category' => Category::CATEGORY_MATH_AND_TRIG,
325 'functionCall' => [MathTrig\Arabic::class, 'evaluate'],
326 'argumentCount' => '1',
327 ],
328 'AREAS' => [
329 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
330 'functionCall' => [Functions::class, 'DUMMY'],
331 'argumentCount' => '1',
332 ],
333 'ARRAYTOTEXT' => [
334 'category' => Category::CATEGORY_TEXT_AND_DATA,
335 'functionCall' => [TextData\Text::class, 'fromArray'],
336 'argumentCount' => '1,2',
337 ],
338 'ASC' => [
339 'category' => Category::CATEGORY_TEXT_AND_DATA,
340 'functionCall' => [Functions::class, 'DUMMY'],
341 'argumentCount' => '1',
342 ],
343 'ASIN' => [
344 'category' => Category::CATEGORY_MATH_AND_TRIG,
345 'functionCall' => [MathTrig\Trig\Sine::class, 'asin'],
346 'argumentCount' => '1',
347 ],
348 'ASINH' => [
349 'category' => Category::CATEGORY_MATH_AND_TRIG,
350 'functionCall' => [MathTrig\Trig\Sine::class, 'asinh'],
351 'argumentCount' => '1',
352 ],
353 'ATAN' => [
354 'category' => Category::CATEGORY_MATH_AND_TRIG,
355 'functionCall' => [MathTrig\Trig\Tangent::class, 'atan'],
356 'argumentCount' => '1',
357 ],
358 'ATAN2' => [
359 'category' => Category::CATEGORY_MATH_AND_TRIG,
360 'functionCall' => [MathTrig\Trig\Tangent::class, 'atan2'],
361 'argumentCount' => '2',
362 ],
363 'ATANH' => [
364 'category' => Category::CATEGORY_MATH_AND_TRIG,
365 'functionCall' => [MathTrig\Trig\Tangent::class, 'atanh'],
366 'argumentCount' => '1',
367 ],
368 'AVEDEV' => [
369 'category' => Category::CATEGORY_STATISTICAL,
370 'functionCall' => [Statistical\Averages::class, 'averageDeviations'],
371 'argumentCount' => '1+',
372 ],
373 'AVERAGE' => [
374 'category' => Category::CATEGORY_STATISTICAL,
375 'functionCall' => [Statistical\Averages::class, 'average'],
376 'argumentCount' => '1+',
377 ],
378 'AVERAGEA' => [
379 'category' => Category::CATEGORY_STATISTICAL,
380 'functionCall' => [Statistical\Averages::class, 'averageA'],
381 'argumentCount' => '1+',
382 ],
383 'AVERAGEIF' => [
384 'category' => Category::CATEGORY_STATISTICAL,
385 'functionCall' => [Statistical\Conditional::class, 'AVERAGEIF'],
386 'argumentCount' => '2,3',
387 ],
388 'AVERAGEIFS' => [
389 'category' => Category::CATEGORY_STATISTICAL,
390 'functionCall' => [Statistical\Conditional::class, 'AVERAGEIFS'],
391 'argumentCount' => '3+',
392 ],
393 'BAHTTEXT' => [
394 'category' => Category::CATEGORY_TEXT_AND_DATA,
395 'functionCall' => [Functions::class, 'DUMMY'],
396 'argumentCount' => '1',
397 ],
398 'BASE' => [
399 'category' => Category::CATEGORY_MATH_AND_TRIG,
400 'functionCall' => [MathTrig\Base::class, 'evaluate'],
401 'argumentCount' => '2,3',
402 ],
403 'BESSELI' => [
404 'category' => Category::CATEGORY_ENGINEERING,
405 'functionCall' => [Engineering\BesselI::class, 'BESSELI'],
406 'argumentCount' => '2',
407 ],
408 'BESSELJ' => [
409 'category' => Category::CATEGORY_ENGINEERING,
410 'functionCall' => [Engineering\BesselJ::class, 'BESSELJ'],
411 'argumentCount' => '2',
412 ],
413 'BESSELK' => [
414 'category' => Category::CATEGORY_ENGINEERING,
415 'functionCall' => [Engineering\BesselK::class, 'BESSELK'],
416 'argumentCount' => '2',
417 ],
418 'BESSELY' => [
419 'category' => Category::CATEGORY_ENGINEERING,
420 'functionCall' => [Engineering\BesselY::class, 'BESSELY'],
421 'argumentCount' => '2',
422 ],
423 'BETADIST' => [
424 'category' => Category::CATEGORY_STATISTICAL,
425 'functionCall' => [Statistical\Distributions\Beta::class, 'distribution'],
426 'argumentCount' => '3-5',
427 ],
428 'BETA.DIST' => [
429 'category' => Category::CATEGORY_STATISTICAL,
430 'functionCall' => [Functions::class, 'DUMMY'],
431 'argumentCount' => '4-6',
432 ],
433 'BETAINV' => [
434 'category' => Category::CATEGORY_STATISTICAL,
435 'functionCall' => [Statistical\Distributions\Beta::class, 'inverse'],
436 'argumentCount' => '3-5',
437 ],
438 'BETA.INV' => [
439 'category' => Category::CATEGORY_STATISTICAL,
440 'functionCall' => [Statistical\Distributions\Beta::class, 'inverse'],
441 'argumentCount' => '3-5',
442 ],
443 'BIN2DEC' => [
444 'category' => Category::CATEGORY_ENGINEERING,
445 'functionCall' => [Engineering\ConvertBinary::class, 'toDecimal'],
446 'argumentCount' => '1',
447 ],
448 'BIN2HEX' => [
449 'category' => Category::CATEGORY_ENGINEERING,
450 'functionCall' => [Engineering\ConvertBinary::class, 'toHex'],
451 'argumentCount' => '1,2',
452 ],
453 'BIN2OCT' => [
454 'category' => Category::CATEGORY_ENGINEERING,
455 'functionCall' => [Engineering\ConvertBinary::class, 'toOctal'],
456 'argumentCount' => '1,2',
457 ],
458 'BINOMDIST' => [
459 'category' => Category::CATEGORY_STATISTICAL,
460 'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'],
461 'argumentCount' => '4',
462 ],
463 'BINOM.DIST' => [
464 'category' => Category::CATEGORY_STATISTICAL,
465 'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'],
466 'argumentCount' => '4',
467 ],
468 'BINOM.DIST.RANGE' => [
469 'category' => Category::CATEGORY_STATISTICAL,
470 'functionCall' => [Statistical\Distributions\Binomial::class, 'range'],
471 'argumentCount' => '3,4',
472 ],
473 'BINOM.INV' => [
474 'category' => Category::CATEGORY_STATISTICAL,
475 'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'],
476 'argumentCount' => '3',
477 ],
478 'BITAND' => [
479 'category' => Category::CATEGORY_ENGINEERING,
480 'functionCall' => [Engineering\BitWise::class, 'BITAND'],
481 'argumentCount' => '2',
482 ],
483 'BITOR' => [
484 'category' => Category::CATEGORY_ENGINEERING,
485 'functionCall' => [Engineering\BitWise::class, 'BITOR'],
486 'argumentCount' => '2',
487 ],
488 'BITXOR' => [
489 'category' => Category::CATEGORY_ENGINEERING,
490 'functionCall' => [Engineering\BitWise::class, 'BITXOR'],
491 'argumentCount' => '2',
492 ],
493 'BITLSHIFT' => [
494 'category' => Category::CATEGORY_ENGINEERING,
495 'functionCall' => [Engineering\BitWise::class, 'BITLSHIFT'],
496 'argumentCount' => '2',
497 ],
498 'BITRSHIFT' => [
499 'category' => Category::CATEGORY_ENGINEERING,
500 'functionCall' => [Engineering\BitWise::class, 'BITRSHIFT'],
501 'argumentCount' => '2',
502 ],
503 'BYCOL' => [
504 'category' => Category::CATEGORY_LOGICAL,
505 'functionCall' => [Functions::class, 'DUMMY'],
506 'argumentCount' => '*',
507 ],
508 'BYROW' => [
509 'category' => Category::CATEGORY_LOGICAL,
510 'functionCall' => [Functions::class, 'DUMMY'],
511 'argumentCount' => '*',
512 ],
513 'CEILING' => [
514 'category' => Category::CATEGORY_MATH_AND_TRIG,
515 'functionCall' => [MathTrig\Ceiling::class, 'ceiling'],
516 'argumentCount' => '1-2', // 2 for Excel, 1-2 for Ods/Gnumeric
517 ],
518 'CEILING.MATH' => [
519 'category' => Category::CATEGORY_MATH_AND_TRIG,
520 'functionCall' => [MathTrig\Ceiling::class, 'math'],
521 'argumentCount' => '1-3',
522 ],
523 'CEILING.PRECISE' => [
524 'category' => Category::CATEGORY_MATH_AND_TRIG,
525 'functionCall' => [MathTrig\Ceiling::class, 'precise'],
526 'argumentCount' => '1,2',
527 ],
528 'CELL' => [
529 'category' => Category::CATEGORY_INFORMATION,
530 'functionCall' => [Functions::class, 'DUMMY'],
531 'argumentCount' => '1,2',
532 ],
533 'CHAR' => [
534 'category' => Category::CATEGORY_TEXT_AND_DATA,
535 'functionCall' => [TextData\CharacterConvert::class, 'character'],
536 'argumentCount' => '1',
537 ],
538 'CHIDIST' => [
539 'category' => Category::CATEGORY_STATISTICAL,
540 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'],
541 'argumentCount' => '2',
542 ],
543 'CHISQ.DIST' => [
544 'category' => Category::CATEGORY_STATISTICAL,
545 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionLeftTail'],
546 'argumentCount' => '3',
547 ],
548 'CHISQ.DIST.RT' => [
549 'category' => Category::CATEGORY_STATISTICAL,
550 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'],
551 'argumentCount' => '2',
552 ],
553 'CHIINV' => [
554 'category' => Category::CATEGORY_STATISTICAL,
555 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'],
556 'argumentCount' => '2',
557 ],
558 'CHISQ.INV' => [
559 'category' => Category::CATEGORY_STATISTICAL,
560 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseLeftTail'],
561 'argumentCount' => '2',
562 ],
563 'CHISQ.INV.RT' => [
564 'category' => Category::CATEGORY_STATISTICAL,
565 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'],
566 'argumentCount' => '2',
567 ],
568 'CHITEST' => [
569 'category' => Category::CATEGORY_STATISTICAL,
570 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'test'],
571 'argumentCount' => '2',
572 ],
573 'CHISQ.TEST' => [
574 'category' => Category::CATEGORY_STATISTICAL,
575 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'test'],
576 'argumentCount' => '2',
577 ],
578 'CHOOSE' => [
579 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
580 'functionCall' => [LookupRef\Selection::class, 'CHOOSE'],
581 'argumentCount' => '2+',
582 ],
583 'CHOOSECOLS' => [
584 'category' => Category::CATEGORY_MATH_AND_TRIG,
585 'functionCall' => [Functions::class, 'DUMMY'],
586 'argumentCount' => '2+',
587 ],
588 'CHOOSEROWS' => [
589 'category' => Category::CATEGORY_MATH_AND_TRIG,
590 'functionCall' => [Functions::class, 'DUMMY'],
591 'argumentCount' => '2+',
592 ],
593 'CLEAN' => [
594 'category' => Category::CATEGORY_TEXT_AND_DATA,
595 'functionCall' => [TextData\Trim::class, 'nonPrintable'],
596 'argumentCount' => '1',
597 ],
598 'CODE' => [
599 'category' => Category::CATEGORY_TEXT_AND_DATA,
600 'functionCall' => [TextData\CharacterConvert::class, 'code'],
601 'argumentCount' => '1',
602 ],
603 'COLUMN' => [
604 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
605 'functionCall' => [LookupRef\RowColumnInformation::class, 'COLUMN'],
606 'argumentCount' => '-1',
607 'passCellReference' => true,
608 'passByReference' => [true],
609 ],
610 'COLUMNS' => [
611 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
612 'functionCall' => [LookupRef\RowColumnInformation::class, 'COLUMNS'],
613 'argumentCount' => '1',
614 ],
615 'COMBIN' => [
616 'category' => Category::CATEGORY_MATH_AND_TRIG,
617 'functionCall' => [MathTrig\Combinations::class, 'withoutRepetition'],
618 'argumentCount' => '2',
619 ],
620 'COMBINA' => [
621 'category' => Category::CATEGORY_MATH_AND_TRIG,
622 'functionCall' => [MathTrig\Combinations::class, 'withRepetition'],
623 'argumentCount' => '2',
624 ],
625 'COMPLEX' => [
626 'category' => Category::CATEGORY_ENGINEERING,
627 'functionCall' => [Engineering\Complex::class, 'COMPLEX'],
628 'argumentCount' => '2,3',
629 ],
630 'CONCAT' => [
631 'category' => Category::CATEGORY_TEXT_AND_DATA,
632 'functionCall' => [TextData\Concatenate::class, 'CONCATENATE'],
633 'argumentCount' => '1+',
634 ],
635 'CONCATENATE' => [
636 'category' => Category::CATEGORY_TEXT_AND_DATA,
637 'functionCall' => [TextData\Concatenate::class, 'CONCATENATE'],
638 'argumentCount' => '1+',
639 ],
640 'CONFIDENCE' => [
641 'category' => Category::CATEGORY_STATISTICAL,
642 'functionCall' => [Statistical\Confidence::class, 'CONFIDENCE'],
643 'argumentCount' => '3',
644 ],
645 'CONFIDENCE.NORM' => [
646 'category' => Category::CATEGORY_STATISTICAL,
647 'functionCall' => [Statistical\Confidence::class, 'CONFIDENCE'],
648 'argumentCount' => '3',
649 ],
650 'CONFIDENCE.T' => [
651 'category' => Category::CATEGORY_STATISTICAL,
652 'functionCall' => [Functions::class, 'DUMMY'],
653 'argumentCount' => '3',
654 ],
655 'CONVERT' => [
656 'category' => Category::CATEGORY_ENGINEERING,
657 'functionCall' => [Engineering\ConvertUOM::class, 'CONVERT'],
658 'argumentCount' => '3',
659 ],
660 'CORREL' => [
661 'category' => Category::CATEGORY_STATISTICAL,
662 'functionCall' => [Statistical\Trends::class, 'CORREL'],
663 'argumentCount' => '2',
664 ],
665 'COS' => [
666 'category' => Category::CATEGORY_MATH_AND_TRIG,
667 'functionCall' => [MathTrig\Trig\Cosine::class, 'cos'],
668 'argumentCount' => '1',
669 ],
670 'COSH' => [
671 'category' => Category::CATEGORY_MATH_AND_TRIG,
672 'functionCall' => [MathTrig\Trig\Cosine::class, 'cosh'],
673 'argumentCount' => '1',
674 ],
675 'COT' => [
676 'category' => Category::CATEGORY_MATH_AND_TRIG,
677 'functionCall' => [MathTrig\Trig\Cotangent::class, 'cot'],
678 'argumentCount' => '1',
679 ],
680 'COTH' => [
681 'category' => Category::CATEGORY_MATH_AND_TRIG,
682 'functionCall' => [MathTrig\Trig\Cotangent::class, 'coth'],
683 'argumentCount' => '1',
684 ],
685 'COUNT' => [
686 'category' => Category::CATEGORY_STATISTICAL,
687 'functionCall' => [Statistical\Counts::class, 'COUNT'],
688 'argumentCount' => '1+',
689 ],
690 'COUNTA' => [
691 'category' => Category::CATEGORY_STATISTICAL,
692 'functionCall' => [Statistical\Counts::class, 'COUNTA'],
693 'argumentCount' => '1+',
694 ],
695 'COUNTBLANK' => [
696 'category' => Category::CATEGORY_STATISTICAL,
697 'functionCall' => [Statistical\Counts::class, 'COUNTBLANK'],
698 'argumentCount' => '1',
699 ],
700 'COUNTIF' => [
701 'category' => Category::CATEGORY_STATISTICAL,
702 'functionCall' => [Statistical\Conditional::class, 'COUNTIF'],
703 'argumentCount' => '2',
704 ],
705 'COUNTIFS' => [
706 'category' => Category::CATEGORY_STATISTICAL,
707 'functionCall' => [Statistical\Conditional::class, 'COUNTIFS'],
708 'argumentCount' => '2+',
709 ],
710 'COUPDAYBS' => [
711 'category' => Category::CATEGORY_FINANCIAL,
712 'functionCall' => [Financial\Coupons::class, 'COUPDAYBS'],
713 'argumentCount' => '3,4',
714 ],
715 'COUPDAYS' => [
716 'category' => Category::CATEGORY_FINANCIAL,
717 'functionCall' => [Financial\Coupons::class, 'COUPDAYS'],
718 'argumentCount' => '3,4',
719 ],
720 'COUPDAYSNC' => [
721 'category' => Category::CATEGORY_FINANCIAL,
722 'functionCall' => [Financial\Coupons::class, 'COUPDAYSNC'],
723 'argumentCount' => '3,4',
724 ],
725 'COUPNCD' => [
726 'category' => Category::CATEGORY_FINANCIAL,
727 'functionCall' => [Financial\Coupons::class, 'COUPNCD'],
728 'argumentCount' => '3,4',
729 ],
730 'COUPNUM' => [
731 'category' => Category::CATEGORY_FINANCIAL,
732 'functionCall' => [Financial\Coupons::class, 'COUPNUM'],
733 'argumentCount' => '3,4',
734 ],
735 'COUPPCD' => [
736 'category' => Category::CATEGORY_FINANCIAL,
737 'functionCall' => [Financial\Coupons::class, 'COUPPCD'],
738 'argumentCount' => '3,4',
739 ],
740 'COVAR' => [
741 'category' => Category::CATEGORY_STATISTICAL,
742 'functionCall' => [Statistical\Trends::class, 'COVAR'],
743 'argumentCount' => '2',
744 ],
745 'COVARIANCE.P' => [
746 'category' => Category::CATEGORY_STATISTICAL,
747 'functionCall' => [Statistical\Trends::class, 'COVAR'],
748 'argumentCount' => '2',
749 ],
750 'COVARIANCE.S' => [
751 'category' => Category::CATEGORY_STATISTICAL,
752 'functionCall' => [Functions::class, 'DUMMY'],
753 'argumentCount' => '2',
754 ],
755 'CRITBINOM' => [
756 'category' => Category::CATEGORY_STATISTICAL,
757 'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'],
758 'argumentCount' => '3',
759 ],
760 'CSC' => [
761 'category' => Category::CATEGORY_MATH_AND_TRIG,
762 'functionCall' => [MathTrig\Trig\Cosecant::class, 'csc'],
763 'argumentCount' => '1',
764 ],
765 'CSCH' => [
766 'category' => Category::CATEGORY_MATH_AND_TRIG,
767 'functionCall' => [MathTrig\Trig\Cosecant::class, 'csch'],
768 'argumentCount' => '1',
769 ],
770 'CUBEKPIMEMBER' => [
771 'category' => Category::CATEGORY_CUBE,
772 'functionCall' => [Functions::class, 'DUMMY'],
773 'argumentCount' => '?',
774 ],
775 'CUBEMEMBER' => [
776 'category' => Category::CATEGORY_CUBE,
777 'functionCall' => [Functions::class, 'DUMMY'],
778 'argumentCount' => '?',
779 ],
780 'CUBEMEMBERPROPERTY' => [
781 'category' => Category::CATEGORY_CUBE,
782 'functionCall' => [Functions::class, 'DUMMY'],
783 'argumentCount' => '?',
784 ],
785 'CUBERANKEDMEMBER' => [
786 'category' => Category::CATEGORY_CUBE,
787 'functionCall' => [Functions::class, 'DUMMY'],
788 'argumentCount' => '?',
789 ],
790 'CUBESET' => [
791 'category' => Category::CATEGORY_CUBE,
792 'functionCall' => [Functions::class, 'DUMMY'],
793 'argumentCount' => '?',
794 ],
795 'CUBESETCOUNT' => [
796 'category' => Category::CATEGORY_CUBE,
797 'functionCall' => [Functions::class, 'DUMMY'],
798 'argumentCount' => '?',
799 ],
800 'CUBEVALUE' => [
801 'category' => Category::CATEGORY_CUBE,
802 'functionCall' => [Functions::class, 'DUMMY'],
803 'argumentCount' => '?',
804 ],
805 'CUMIPMT' => [
806 'category' => Category::CATEGORY_FINANCIAL,
807 'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'interest'],
808 'argumentCount' => '6',
809 ],
810 'CUMPRINC' => [
811 'category' => Category::CATEGORY_FINANCIAL,
812 'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'principal'],
813 'argumentCount' => '6',
814 ],
815 'DATE' => [
816 'category' => Category::CATEGORY_DATE_AND_TIME,
817 'functionCall' => [DateTimeExcel\Date::class, 'fromYMD'],
818 'argumentCount' => '3',
819 ],
820 'DATEDIF' => [
821 'category' => Category::CATEGORY_DATE_AND_TIME,
822 'functionCall' => [DateTimeExcel\Difference::class, 'interval'],
823 'argumentCount' => '2,3',
824 ],
825 'DATESTRING' => [
826 'category' => Category::CATEGORY_DATE_AND_TIME,
827 'functionCall' => [Functions::class, 'DUMMY'],
828 'argumentCount' => '?',
829 ],
830 'DATEVALUE' => [
831 'category' => Category::CATEGORY_DATE_AND_TIME,
832 'functionCall' => [DateTimeExcel\DateValue::class, 'fromString'],
833 'argumentCount' => '1',
834 ],
835 'DAVERAGE' => [
836 'category' => Category::CATEGORY_DATABASE,
837 'functionCall' => [Database\DAverage::class, 'evaluate'],
838 'argumentCount' => '3',
839 ],
840 'DAY' => [
841 'category' => Category::CATEGORY_DATE_AND_TIME,
842 'functionCall' => [DateTimeExcel\DateParts::class, 'day'],
843 'argumentCount' => '1',
844 ],
845 'DAYS' => [
846 'category' => Category::CATEGORY_DATE_AND_TIME,
847 'functionCall' => [DateTimeExcel\Days::class, 'between'],
848 'argumentCount' => '2',
849 ],
850 'DAYS360' => [
851 'category' => Category::CATEGORY_DATE_AND_TIME,
852 'functionCall' => [DateTimeExcel\Days360::class, 'between'],
853 'argumentCount' => '2,3',
854 ],
855 'DB' => [
856 'category' => Category::CATEGORY_FINANCIAL,
857 'functionCall' => [Financial\Depreciation::class, 'DB'],
858 'argumentCount' => '4,5',
859 ],
860 'DBCS' => [
861 'category' => Category::CATEGORY_TEXT_AND_DATA,
862 'functionCall' => [Functions::class, 'DUMMY'],
863 'argumentCount' => '1',
864 ],
865 'DCOUNT' => [
866 'category' => Category::CATEGORY_DATABASE,
867 'functionCall' => [Database\DCount::class, 'evaluate'],
868 'argumentCount' => '3',
869 ],
870 'DCOUNTA' => [
871 'category' => Category::CATEGORY_DATABASE,
872 'functionCall' => [Database\DCountA::class, 'evaluate'],
873 'argumentCount' => '3',
874 ],
875 'DDB' => [
876 'category' => Category::CATEGORY_FINANCIAL,
877 'functionCall' => [Financial\Depreciation::class, 'DDB'],
878 'argumentCount' => '4,5',
879 ],
880 'DEC2BIN' => [
881 'category' => Category::CATEGORY_ENGINEERING,
882 'functionCall' => [Engineering\ConvertDecimal::class, 'toBinary'],
883 'argumentCount' => '1,2',
884 ],
885 'DEC2HEX' => [
886 'category' => Category::CATEGORY_ENGINEERING,
887 'functionCall' => [Engineering\ConvertDecimal::class, 'toHex'],
888 'argumentCount' => '1,2',
889 ],
890 'DEC2OCT' => [
891 'category' => Category::CATEGORY_ENGINEERING,
892 'functionCall' => [Engineering\ConvertDecimal::class, 'toOctal'],
893 'argumentCount' => '1,2',
894 ],
895 'DECIMAL' => [
896 'category' => Category::CATEGORY_MATH_AND_TRIG,
897 'functionCall' => [Functions::class, 'DUMMY'],
898 'argumentCount' => '2',
899 ],
900 'DEGREES' => [
901 'category' => Category::CATEGORY_MATH_AND_TRIG,
902 'functionCall' => [MathTrig\Angle::class, 'toDegrees'],
903 'argumentCount' => '1',
904 ],
905 'DELTA' => [
906 'category' => Category::CATEGORY_ENGINEERING,
907 'functionCall' => [Engineering\Compare::class, 'DELTA'],
908 'argumentCount' => '1,2',
909 ],
910 'DEVSQ' => [
911 'category' => Category::CATEGORY_STATISTICAL,
912 'functionCall' => [Statistical\Deviations::class, 'sumSquares'],
913 'argumentCount' => '1+',
914 ],
915 'DGET' => [
916 'category' => Category::CATEGORY_DATABASE,
917 'functionCall' => [Database\DGet::class, 'evaluate'],
918 'argumentCount' => '3',
919 ],
920 'DISC' => [
921 'category' => Category::CATEGORY_FINANCIAL,
922 'functionCall' => [Financial\Securities\Rates::class, 'discount'],
923 'argumentCount' => '4,5',
924 ],
925 'DMAX' => [
926 'category' => Category::CATEGORY_DATABASE,
927 'functionCall' => [Database\DMax::class, 'evaluate'],
928 'argumentCount' => '3',
929 ],
930 'DMIN' => [
931 'category' => Category::CATEGORY_DATABASE,
932 'functionCall' => [Database\DMin::class, 'evaluate'],
933 'argumentCount' => '3',
934 ],
935 'DOLLAR' => [
936 'category' => Category::CATEGORY_TEXT_AND_DATA,
937 'functionCall' => [TextData\Format::class, 'DOLLAR'],
938 'argumentCount' => '1,2',
939 ],
940 'DOLLARDE' => [
941 'category' => Category::CATEGORY_FINANCIAL,
942 'functionCall' => [Financial\Dollar::class, 'decimal'],
943 'argumentCount' => '2',
944 ],
945 'DOLLARFR' => [
946 'category' => Category::CATEGORY_FINANCIAL,
947 'functionCall' => [Financial\Dollar::class, 'fractional'],
948 'argumentCount' => '2',
949 ],
950 'DPRODUCT' => [
951 'category' => Category::CATEGORY_DATABASE,
952 'functionCall' => [Database\DProduct::class, 'evaluate'],
953 'argumentCount' => '3',
954 ],
955 'DROP' => [
956 'category' => Category::CATEGORY_MATH_AND_TRIG,
957 'functionCall' => [Functions::class, 'DUMMY'],
958 'argumentCount' => '2-3',
959 ],
960 'DSTDEV' => [
961 'category' => Category::CATEGORY_DATABASE,
962 'functionCall' => [Database\DStDev::class, 'evaluate'],
963 'argumentCount' => '3',
964 ],
965 'DSTDEVP' => [
966 'category' => Category::CATEGORY_DATABASE,
967 'functionCall' => [Database\DStDevP::class, 'evaluate'],
968 'argumentCount' => '3',
969 ],
970 'DSUM' => [
971 'category' => Category::CATEGORY_DATABASE,
972 'functionCall' => [Database\DSum::class, 'evaluate'],
973 'argumentCount' => '3',
974 ],
975 'DURATION' => [
976 'category' => Category::CATEGORY_FINANCIAL,
977 'functionCall' => [Functions::class, 'DUMMY'],
978 'argumentCount' => '5,6',
979 ],
980 'DVAR' => [
981 'category' => Category::CATEGORY_DATABASE,
982 'functionCall' => [Database\DVar::class, 'evaluate'],
983 'argumentCount' => '3',
984 ],
985 'DVARP' => [
986 'category' => Category::CATEGORY_DATABASE,
987 'functionCall' => [Database\DVarP::class, 'evaluate'],
988 'argumentCount' => '3',
989 ],
990 'ECMA.CEILING' => [
991 'category' => Category::CATEGORY_MATH_AND_TRIG,
992 'functionCall' => [Functions::class, 'DUMMY'],
993 'argumentCount' => '1,2',
994 ],
995 'EDATE' => [
996 'category' => Category::CATEGORY_DATE_AND_TIME,
997 'functionCall' => [DateTimeExcel\Month::class, 'adjust'],
998 'argumentCount' => '2',
999 ],
1000 'EFFECT' => [
1001 'category' => Category::CATEGORY_FINANCIAL,
1002 'functionCall' => [Financial\InterestRate::class, 'effective'],
1003 'argumentCount' => '2',
1004 ],
1005 'ENCODEURL' => [
1006 'category' => Category::CATEGORY_WEB,
1007 'functionCall' => [Web\Service::class, 'urlEncode'],
1008 'argumentCount' => '1',
1009 ],
1010 'EOMONTH' => [
1011 'category' => Category::CATEGORY_DATE_AND_TIME,
1012 'functionCall' => [DateTimeExcel\Month::class, 'lastDay'],
1013 'argumentCount' => '2',
1014 ],
1015 'ERF' => [
1016 'category' => Category::CATEGORY_ENGINEERING,
1017 'functionCall' => [Engineering\Erf::class, 'ERF'],
1018 'argumentCount' => '1,2',
1019 ],
1020 'ERF.PRECISE' => [
1021 'category' => Category::CATEGORY_ENGINEERING,
1022 'functionCall' => [Engineering\Erf::class, 'ERFPRECISE'],
1023 'argumentCount' => '1',
1024 ],
1025 'ERFC' => [
1026 'category' => Category::CATEGORY_ENGINEERING,
1027 'functionCall' => [Engineering\ErfC::class, 'ERFC'],
1028 'argumentCount' => '1',
1029 ],
1030 'ERFC.PRECISE' => [
1031 'category' => Category::CATEGORY_ENGINEERING,
1032 'functionCall' => [Engineering\ErfC::class, 'ERFC'],
1033 'argumentCount' => '1',
1034 ],
1035 'ERROR.TYPE' => [
1036 'category' => Category::CATEGORY_INFORMATION,
1037 'functionCall' => [Information\ExcelError::class, 'type'],
1038 'argumentCount' => '1',
1039 ],
1040 'EVEN' => [
1041 'category' => Category::CATEGORY_MATH_AND_TRIG,
1042 'functionCall' => [MathTrig\Round::class, 'even'],
1043 'argumentCount' => '1',
1044 ],
1045 'EXACT' => [
1046 'category' => Category::CATEGORY_TEXT_AND_DATA,
1047 'functionCall' => [TextData\Text::class, 'exact'],
1048 'argumentCount' => '2',
1049 ],
1050 'EXP' => [
1051 'category' => Category::CATEGORY_MATH_AND_TRIG,
1052 'functionCall' => [MathTrig\Exp::class, 'evaluate'],
1053 'argumentCount' => '1',
1054 ],
1055 'EXPAND' => [
1056 'category' => Category::CATEGORY_MATH_AND_TRIG,
1057 'functionCall' => [Functions::class, 'DUMMY'],
1058 'argumentCount' => '2-4',
1059 ],
1060 'EXPONDIST' => [
1061 'category' => Category::CATEGORY_STATISTICAL,
1062 'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'],
1063 'argumentCount' => '3',
1064 ],
1065 'EXPON.DIST' => [
1066 'category' => Category::CATEGORY_STATISTICAL,
1067 'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'],
1068 'argumentCount' => '3',
1069 ],
1070 'FACT' => [
1071 'category' => Category::CATEGORY_MATH_AND_TRIG,
1072 'functionCall' => [MathTrig\Factorial::class, 'fact'],
1073 'argumentCount' => '1',
1074 ],
1075 'FACTDOUBLE' => [
1076 'category' => Category::CATEGORY_MATH_AND_TRIG,
1077 'functionCall' => [MathTrig\Factorial::class, 'factDouble'],
1078 'argumentCount' => '1',
1079 ],
1080 'FALSE' => [
1081 'category' => Category::CATEGORY_LOGICAL,
1082 'functionCall' => [Logical\Boolean::class, 'FALSE'],
1083 'argumentCount' => '0',
1084 ],
1085 'FDIST' => [
1086 'category' => Category::CATEGORY_STATISTICAL,
1087 'functionCall' => [Functions::class, 'DUMMY'],
1088 'argumentCount' => '3',
1089 ],
1090 'F.DIST' => [
1091 'category' => Category::CATEGORY_STATISTICAL,
1092 'functionCall' => [Statistical\Distributions\F::class, 'distribution'],
1093 'argumentCount' => '4',
1094 ],
1095 'F.DIST.RT' => [
1096 'category' => Category::CATEGORY_STATISTICAL,
1097 'functionCall' => [Functions::class, 'DUMMY'],
1098 'argumentCount' => '3',
1099 ],
1100 'FILTER' => [
1101 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1102 'functionCall' => [LookupRef\Filter::class, 'filter'],
1103 'argumentCount' => '2-3',
1104 ],
1105 'FILTERXML' => [
1106 'category' => Category::CATEGORY_WEB,
1107 'functionCall' => [Functions::class, 'DUMMY'],
1108 'argumentCount' => '2',
1109 ],
1110 'FIND' => [
1111 'category' => Category::CATEGORY_TEXT_AND_DATA,
1112 'functionCall' => [TextData\Search::class, 'sensitive'],
1113 'argumentCount' => '2,3',
1114 ],
1115 'FINDB' => [
1116 'category' => Category::CATEGORY_TEXT_AND_DATA,
1117 'functionCall' => [TextData\Search::class, 'sensitive'],
1118 'argumentCount' => '2,3',
1119 ],
1120 'FINV' => [
1121 'category' => Category::CATEGORY_STATISTICAL,
1122 'functionCall' => [Functions::class, 'DUMMY'],
1123 'argumentCount' => '3',
1124 ],
1125 'F.INV' => [
1126 'category' => Category::CATEGORY_STATISTICAL,
1127 'functionCall' => [Functions::class, 'DUMMY'],
1128 'argumentCount' => '3',
1129 ],
1130 'F.INV.RT' => [
1131 'category' => Category::CATEGORY_STATISTICAL,
1132 'functionCall' => [Functions::class, 'DUMMY'],
1133 'argumentCount' => '3',
1134 ],
1135 'FISHER' => [
1136 'category' => Category::CATEGORY_STATISTICAL,
1137 'functionCall' => [Statistical\Distributions\Fisher::class, 'distribution'],
1138 'argumentCount' => '1',
1139 ],
1140 'FISHERINV' => [
1141 'category' => Category::CATEGORY_STATISTICAL,
1142 'functionCall' => [Statistical\Distributions\Fisher::class, 'inverse'],
1143 'argumentCount' => '1',
1144 ],
1145 'FIXED' => [
1146 'category' => Category::CATEGORY_TEXT_AND_DATA,
1147 'functionCall' => [TextData\Format::class, 'FIXEDFORMAT'],
1148 'argumentCount' => '1-3',
1149 ],
1150 'FLOOR' => [
1151 'category' => Category::CATEGORY_MATH_AND_TRIG,
1152 'functionCall' => [MathTrig\Floor::class, 'floor'],
1153 'argumentCount' => '1-2', // Excel requries 2, Ods/Gnumeric 1-2
1154 ],
1155 'FLOOR.MATH' => [
1156 'category' => Category::CATEGORY_MATH_AND_TRIG,
1157 'functionCall' => [MathTrig\Floor::class, 'math'],
1158 'argumentCount' => '1-3',
1159 ],
1160 'FLOOR.PRECISE' => [
1161 'category' => Category::CATEGORY_MATH_AND_TRIG,
1162 'functionCall' => [MathTrig\Floor::class, 'precise'],
1163 'argumentCount' => '1-2',
1164 ],
1165 'FORECAST' => [
1166 'category' => Category::CATEGORY_STATISTICAL,
1167 'functionCall' => [Statistical\Trends::class, 'FORECAST'],
1168 'argumentCount' => '3',
1169 ],
1170 'FORECAST.ETS' => [
1171 'category' => Category::CATEGORY_STATISTICAL,
1172 'functionCall' => [Functions::class, 'DUMMY'],
1173 'argumentCount' => '3-6',
1174 ],
1175 'FORECAST.ETS.CONFINT' => [
1176 'category' => Category::CATEGORY_STATISTICAL,
1177 'functionCall' => [Functions::class, 'DUMMY'],
1178 'argumentCount' => '3-6',
1179 ],
1180 'FORECAST.ETS.SEASONALITY' => [
1181 'category' => Category::CATEGORY_STATISTICAL,
1182 'functionCall' => [Functions::class, 'DUMMY'],
1183 'argumentCount' => '2-4',
1184 ],
1185 'FORECAST.ETS.STAT' => [
1186 'category' => Category::CATEGORY_STATISTICAL,
1187 'functionCall' => [Functions::class, 'DUMMY'],
1188 'argumentCount' => '3-6',
1189 ],
1190 'FORECAST.LINEAR' => [
1191 'category' => Category::CATEGORY_STATISTICAL,
1192 'functionCall' => [Statistical\Trends::class, 'FORECAST'],
1193 'argumentCount' => '3',
1194 ],
1195 'FORMULATEXT' => [
1196 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1197 'functionCall' => [LookupRef\Formula::class, 'text'],
1198 'argumentCount' => '1',
1199 'passCellReference' => true,
1200 'passByReference' => [true],
1201 ],
1202 'FREQUENCY' => [
1203 'category' => Category::CATEGORY_STATISTICAL,
1204 'functionCall' => [Functions::class, 'DUMMY'],
1205 'argumentCount' => '2',
1206 ],
1207 'FTEST' => [
1208 'category' => Category::CATEGORY_STATISTICAL,
1209 'functionCall' => [Functions::class, 'DUMMY'],
1210 'argumentCount' => '2',
1211 ],
1212 'F.TEST' => [
1213 'category' => Category::CATEGORY_STATISTICAL,
1214 'functionCall' => [Functions::class, 'DUMMY'],
1215 'argumentCount' => '2',
1216 ],
1217 'FV' => [
1218 'category' => Category::CATEGORY_FINANCIAL,
1219 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'futureValue'],
1220 'argumentCount' => '3-5',
1221 ],
1222 'FVSCHEDULE' => [
1223 'category' => Category::CATEGORY_FINANCIAL,
1224 'functionCall' => [Financial\CashFlow\Single::class, 'futureValue'],
1225 'argumentCount' => '2',
1226 ],
1227 'GAMMA' => [
1228 'category' => Category::CATEGORY_STATISTICAL,
1229 'functionCall' => [Statistical\Distributions\Gamma::class, 'gamma'],
1230 'argumentCount' => '1',
1231 ],
1232 'GAMMADIST' => [
1233 'category' => Category::CATEGORY_STATISTICAL,
1234 'functionCall' => [Statistical\Distributions\Gamma::class, 'distribution'],
1235 'argumentCount' => '4',
1236 ],
1237 'GAMMA.DIST' => [
1238 'category' => Category::CATEGORY_STATISTICAL,
1239 'functionCall' => [Statistical\Distributions\Gamma::class, 'distribution'],
1240 'argumentCount' => '4',
1241 ],
1242 'GAMMAINV' => [
1243 'category' => Category::CATEGORY_STATISTICAL,
1244 'functionCall' => [Statistical\Distributions\Gamma::class, 'inverse'],
1245 'argumentCount' => '3',
1246 ],
1247 'GAMMA.INV' => [
1248 'category' => Category::CATEGORY_STATISTICAL,
1249 'functionCall' => [Statistical\Distributions\Gamma::class, 'inverse'],
1250 'argumentCount' => '3',
1251 ],
1252 'GAMMALN' => [
1253 'category' => Category::CATEGORY_STATISTICAL,
1254 'functionCall' => [Statistical\Distributions\Gamma::class, 'ln'],
1255 'argumentCount' => '1',
1256 ],
1257 'GAMMALN.PRECISE' => [
1258 'category' => Category::CATEGORY_STATISTICAL,
1259 'functionCall' => [Statistical\Distributions\Gamma::class, 'ln'],
1260 'argumentCount' => '1',
1261 ],
1262 'GAUSS' => [
1263 'category' => Category::CATEGORY_STATISTICAL,
1264 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'gauss'],
1265 'argumentCount' => '1',
1266 ],
1267 'GCD' => [
1268 'category' => Category::CATEGORY_MATH_AND_TRIG,
1269 'functionCall' => [MathTrig\Gcd::class, 'evaluate'],
1270 'argumentCount' => '1+',
1271 ],
1272 'GEOMEAN' => [
1273 'category' => Category::CATEGORY_STATISTICAL,
1274 'functionCall' => [Statistical\Averages\Mean::class, 'geometric'],
1275 'argumentCount' => '1+',
1276 ],
1277 'GESTEP' => [
1278 'category' => Category::CATEGORY_ENGINEERING,
1279 'functionCall' => [Engineering\Compare::class, 'GESTEP'],
1280 'argumentCount' => '1,2',
1281 ],
1282 'GETPIVOTDATA' => [
1283 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1284 'functionCall' => [Functions::class, 'DUMMY'],
1285 'argumentCount' => '2+',
1286 ],
1287 'GROWTH' => [
1288 'category' => Category::CATEGORY_STATISTICAL,
1289 'functionCall' => [Statistical\Trends::class, 'GROWTH'],
1290 'argumentCount' => '1-4',
1291 ],
1292 'HARMEAN' => [
1293 'category' => Category::CATEGORY_STATISTICAL,
1294 'functionCall' => [Statistical\Averages\Mean::class, 'harmonic'],
1295 'argumentCount' => '1+',
1296 ],
1297 'HEX2BIN' => [
1298 'category' => Category::CATEGORY_ENGINEERING,
1299 'functionCall' => [Engineering\ConvertHex::class, 'toBinary'],
1300 'argumentCount' => '1,2',
1301 ],
1302 'HEX2DEC' => [
1303 'category' => Category::CATEGORY_ENGINEERING,
1304 'functionCall' => [Engineering\ConvertHex::class, 'toDecimal'],
1305 'argumentCount' => '1',
1306 ],
1307 'HEX2OCT' => [
1308 'category' => Category::CATEGORY_ENGINEERING,
1309 'functionCall' => [Engineering\ConvertHex::class, 'toOctal'],
1310 'argumentCount' => '1,2',
1311 ],
1312 'HLOOKUP' => [
1313 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1314 'functionCall' => [LookupRef\HLookup::class, 'lookup'],
1315 'argumentCount' => '3,4',
1316 ],
1317 'HOUR' => [
1318 'category' => Category::CATEGORY_DATE_AND_TIME,
1319 'functionCall' => [DateTimeExcel\TimeParts::class, 'hour'],
1320 'argumentCount' => '1',
1321 ],
1322 'HSTACK' => [
1323 'category' => Category::CATEGORY_MATH_AND_TRIG,
1324 'functionCall' => [Functions::class, 'DUMMY'],
1325 'argumentCount' => '1+',
1326 ],
1327 'HYPERLINK' => [
1328 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1329 'functionCall' => [LookupRef\Hyperlink::class, 'set'],
1330 'argumentCount' => '1,2',
1331 'passCellReference' => true,
1332 ],
1333 'HYPGEOMDIST' => [
1334 'category' => Category::CATEGORY_STATISTICAL,
1335 'functionCall' => [Statistical\Distributions\HyperGeometric::class, 'distribution'],
1336 'argumentCount' => '4',
1337 ],
1338 'HYPGEOM.DIST' => [
1339 'category' => Category::CATEGORY_STATISTICAL,
1340 'functionCall' => [Functions::class, 'DUMMY'],
1341 'argumentCount' => '5',
1342 ],
1343 'IF' => [
1344 'category' => Category::CATEGORY_LOGICAL,
1345 'functionCall' => [Logical\Conditional::class, 'statementIf'],
1346 'argumentCount' => '1-3',
1347 ],
1348 'IFERROR' => [
1349 'category' => Category::CATEGORY_LOGICAL,
1350 'functionCall' => [Logical\Conditional::class, 'IFERROR'],
1351 'argumentCount' => '2',
1352 ],
1353 'IFNA' => [
1354 'category' => Category::CATEGORY_LOGICAL,
1355 'functionCall' => [Logical\Conditional::class, 'IFNA'],
1356 'argumentCount' => '2',
1357 ],
1358 'IFS' => [
1359 'category' => Category::CATEGORY_LOGICAL,
1360 'functionCall' => [Logical\Conditional::class, 'IFS'],
1361 'argumentCount' => '2+',
1362 ],
1363 'IMABS' => [
1364 'category' => Category::CATEGORY_ENGINEERING,
1365 'functionCall' => [Engineering\ComplexFunctions::class, 'IMABS'],
1366 'argumentCount' => '1',
1367 ],
1368 'IMAGINARY' => [
1369 'category' => Category::CATEGORY_ENGINEERING,
1370 'functionCall' => [Engineering\Complex::class, 'IMAGINARY'],
1371 'argumentCount' => '1',
1372 ],
1373 'IMARGUMENT' => [
1374 'category' => Category::CATEGORY_ENGINEERING,
1375 'functionCall' => [Engineering\ComplexFunctions::class, 'IMARGUMENT'],
1376 'argumentCount' => '1',
1377 ],
1378 'IMCONJUGATE' => [
1379 'category' => Category::CATEGORY_ENGINEERING,
1380 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCONJUGATE'],
1381 'argumentCount' => '1',
1382 ],
1383 'IMCOS' => [
1384 'category' => Category::CATEGORY_ENGINEERING,
1385 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOS'],
1386 'argumentCount' => '1',
1387 ],
1388 'IMCOSH' => [
1389 'category' => Category::CATEGORY_ENGINEERING,
1390 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOSH'],
1391 'argumentCount' => '1',
1392 ],
1393 'IMCOT' => [
1394 'category' => Category::CATEGORY_ENGINEERING,
1395 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOT'],
1396 'argumentCount' => '1',
1397 ],
1398 'IMCSC' => [
1399 'category' => Category::CATEGORY_ENGINEERING,
1400 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCSC'],
1401 'argumentCount' => '1',
1402 ],
1403 'IMCSCH' => [
1404 'category' => Category::CATEGORY_ENGINEERING,
1405 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCSCH'],
1406 'argumentCount' => '1',
1407 ],
1408 'IMDIV' => [
1409 'category' => Category::CATEGORY_ENGINEERING,
1410 'functionCall' => [Engineering\ComplexOperations::class, 'IMDIV'],
1411 'argumentCount' => '2',
1412 ],
1413 'IMEXP' => [
1414 'category' => Category::CATEGORY_ENGINEERING,
1415 'functionCall' => [Engineering\ComplexFunctions::class, 'IMEXP'],
1416 'argumentCount' => '1',
1417 ],
1418 'IMLN' => [
1419 'category' => Category::CATEGORY_ENGINEERING,
1420 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLN'],
1421 'argumentCount' => '1',
1422 ],
1423 'IMLOG10' => [
1424 'category' => Category::CATEGORY_ENGINEERING,
1425 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLOG10'],
1426 'argumentCount' => '1',
1427 ],
1428 'IMLOG2' => [
1429 'category' => Category::CATEGORY_ENGINEERING,
1430 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLOG2'],
1431 'argumentCount' => '1',
1432 ],
1433 'IMPOWER' => [
1434 'category' => Category::CATEGORY_ENGINEERING,
1435 'functionCall' => [Engineering\ComplexFunctions::class, 'IMPOWER'],
1436 'argumentCount' => '2',
1437 ],
1438 'IMPRODUCT' => [
1439 'category' => Category::CATEGORY_ENGINEERING,
1440 'functionCall' => [Engineering\ComplexOperations::class, 'IMPRODUCT'],
1441 'argumentCount' => '1+',
1442 ],
1443 'IMREAL' => [
1444 'category' => Category::CATEGORY_ENGINEERING,
1445 'functionCall' => [Engineering\Complex::class, 'IMREAL'],
1446 'argumentCount' => '1',
1447 ],
1448 'IMSEC' => [
1449 'category' => Category::CATEGORY_ENGINEERING,
1450 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSEC'],
1451 'argumentCount' => '1',
1452 ],
1453 'IMSECH' => [
1454 'category' => Category::CATEGORY_ENGINEERING,
1455 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSECH'],
1456 'argumentCount' => '1',
1457 ],
1458 'IMSIN' => [
1459 'category' => Category::CATEGORY_ENGINEERING,
1460 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSIN'],
1461 'argumentCount' => '1',
1462 ],
1463 'IMSINH' => [
1464 'category' => Category::CATEGORY_ENGINEERING,
1465 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSINH'],
1466 'argumentCount' => '1',
1467 ],
1468 'IMSQRT' => [
1469 'category' => Category::CATEGORY_ENGINEERING,
1470 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSQRT'],
1471 'argumentCount' => '1',
1472 ],
1473 'IMSUB' => [
1474 'category' => Category::CATEGORY_ENGINEERING,
1475 'functionCall' => [Engineering\ComplexOperations::class, 'IMSUB'],
1476 'argumentCount' => '2',
1477 ],
1478 'IMSUM' => [
1479 'category' => Category::CATEGORY_ENGINEERING,
1480 'functionCall' => [Engineering\ComplexOperations::class, 'IMSUM'],
1481 'argumentCount' => '1+',
1482 ],
1483 'IMTAN' => [
1484 'category' => Category::CATEGORY_ENGINEERING,
1485 'functionCall' => [Engineering\ComplexFunctions::class, 'IMTAN'],
1486 'argumentCount' => '1',
1487 ],
1488 'INDEX' => [
1489 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1490 'functionCall' => [LookupRef\Matrix::class, 'index'],
1491 'argumentCount' => '2-4',
1492 ],
1493 'INDIRECT' => [
1494 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1495 'functionCall' => [LookupRef\Indirect::class, 'INDIRECT'],
1496 'argumentCount' => '1,2',
1497 'passCellReference' => true,
1498 ],
1499 'INFO' => [
1500 'category' => Category::CATEGORY_INFORMATION,
1501 'functionCall' => [Functions::class, 'DUMMY'],
1502 'argumentCount' => '1',
1503 ],
1504 'INT' => [
1505 'category' => Category::CATEGORY_MATH_AND_TRIG,
1506 'functionCall' => [MathTrig\IntClass::class, 'evaluate'],
1507 'argumentCount' => '1',
1508 ],
1509 'INTERCEPT' => [
1510 'category' => Category::CATEGORY_STATISTICAL,
1511 'functionCall' => [Statistical\Trends::class, 'INTERCEPT'],
1512 'argumentCount' => '2',
1513 ],
1514 'INTRATE' => [
1515 'category' => Category::CATEGORY_FINANCIAL,
1516 'functionCall' => [Financial\Securities\Rates::class, 'interest'],
1517 'argumentCount' => '4,5',
1518 ],
1519 'IPMT' => [
1520 'category' => Category::CATEGORY_FINANCIAL,
1521 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'payment'],
1522 'argumentCount' => '4-6',
1523 ],
1524 'IRR' => [
1525 'category' => Category::CATEGORY_FINANCIAL,
1526 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'rate'],
1527 'argumentCount' => '1,2',
1528 ],
1529 'ISBLANK' => [
1530 'category' => Category::CATEGORY_INFORMATION,
1531 'functionCall' => [Information\Value::class, 'isBlank'],
1532 'argumentCount' => '1',
1533 ],
1534 'ISERR' => [
1535 'category' => Category::CATEGORY_INFORMATION,
1536 'functionCall' => [Information\ErrorValue::class, 'isErr'],
1537 'argumentCount' => '1',
1538 ],
1539 'ISERROR' => [
1540 'category' => Category::CATEGORY_INFORMATION,
1541 'functionCall' => [Information\ErrorValue::class, 'isError'],
1542 'argumentCount' => '1',
1543 ],
1544 'ISEVEN' => [
1545 'category' => Category::CATEGORY_INFORMATION,
1546 'functionCall' => [Information\Value::class, 'isEven'],
1547 'argumentCount' => '1',
1548 ],
1549 'ISFORMULA' => [
1550 'category' => Category::CATEGORY_INFORMATION,
1551 'functionCall' => [Information\Value::class, 'isFormula'],
1552 'argumentCount' => '1',
1553 'passCellReference' => true,
1554 'passByReference' => [true],
1555 ],
1556 'ISLOGICAL' => [
1557 'category' => Category::CATEGORY_INFORMATION,
1558 'functionCall' => [Information\Value::class, 'isLogical'],
1559 'argumentCount' => '1',
1560 ],
1561 'ISNA' => [
1562 'category' => Category::CATEGORY_INFORMATION,
1563 'functionCall' => [Information\ErrorValue::class, 'isNa'],
1564 'argumentCount' => '1',
1565 ],
1566 'ISNONTEXT' => [
1567 'category' => Category::CATEGORY_INFORMATION,
1568 'functionCall' => [Information\Value::class, 'isNonText'],
1569 'argumentCount' => '1',
1570 ],
1571 'ISNUMBER' => [
1572 'category' => Category::CATEGORY_INFORMATION,
1573 'functionCall' => [Information\Value::class, 'isNumber'],
1574 'argumentCount' => '1',
1575 ],
1576 'ISO.CEILING' => [
1577 'category' => Category::CATEGORY_MATH_AND_TRIG,
1578 'functionCall' => [Functions::class, 'DUMMY'],
1579 'argumentCount' => '1,2',
1580 ],
1581 'ISODD' => [
1582 'category' => Category::CATEGORY_INFORMATION,
1583 'functionCall' => [Information\Value::class, 'isOdd'],
1584 'argumentCount' => '1',
1585 ],
1586 'ISOMITTED' => [
1587 'category' => Category::CATEGORY_INFORMATION,
1588 'functionCall' => [Functions::class, 'DUMMY'],
1589 'argumentCount' => '*',
1590 ],
1591 'ISOWEEKNUM' => [
1592 'category' => Category::CATEGORY_DATE_AND_TIME,
1593 'functionCall' => [DateTimeExcel\Week::class, 'isoWeekNumber'],
1594 'argumentCount' => '1',
1595 ],
1596 'ISPMT' => [
1597 'category' => Category::CATEGORY_FINANCIAL,
1598 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'schedulePayment'],
1599 'argumentCount' => '4',
1600 ],
1601 'ISREF' => [
1602 'category' => Category::CATEGORY_INFORMATION,
1603 'functionCall' => [Information\Value::class, 'isRef'],
1604 'argumentCount' => '1',
1605 'passCellReference' => true,
1606 'passByReference' => [true],
1607 ],
1608 'ISTEXT' => [
1609 'category' => Category::CATEGORY_INFORMATION,
1610 'functionCall' => [Information\Value::class, 'isText'],
1611 'argumentCount' => '1',
1612 ],
1613 'ISTHAIDIGIT' => [
1614 'category' => Category::CATEGORY_TEXT_AND_DATA,
1615 'functionCall' => [Functions::class, 'DUMMY'],
1616 'argumentCount' => '?',
1617 ],
1618 'JIS' => [
1619 'category' => Category::CATEGORY_TEXT_AND_DATA,
1620 'functionCall' => [Functions::class, 'DUMMY'],
1621 'argumentCount' => '1',
1622 ],
1623 'KURT' => [
1624 'category' => Category::CATEGORY_STATISTICAL,
1625 'functionCall' => [Statistical\Deviations::class, 'kurtosis'],
1626 'argumentCount' => '1+',
1627 ],
1628 'LAMBDA' => [
1629 'category' => Category::CATEGORY_LOGICAL,
1630 'functionCall' => [Functions::class, 'DUMMY'],
1631 'argumentCount' => '*',
1632 ],
1633 'LARGE' => [
1634 'category' => Category::CATEGORY_STATISTICAL,
1635 'functionCall' => [Statistical\Size::class, 'large'],
1636 'argumentCount' => '2',
1637 ],
1638 'LCM' => [
1639 'category' => Category::CATEGORY_MATH_AND_TRIG,
1640 'functionCall' => [MathTrig\Lcm::class, 'evaluate'],
1641 'argumentCount' => '1+',
1642 ],
1643 'LEFT' => [
1644 'category' => Category::CATEGORY_TEXT_AND_DATA,
1645 'functionCall' => [TextData\Extract::class, 'left'],
1646 'argumentCount' => '1,2',
1647 ],
1648 'LEFTB' => [
1649 'category' => Category::CATEGORY_TEXT_AND_DATA,
1650 'functionCall' => [TextData\Extract::class, 'left'],
1651 'argumentCount' => '1,2',
1652 ],
1653 'LEN' => [
1654 'category' => Category::CATEGORY_TEXT_AND_DATA,
1655 'functionCall' => [TextData\Text::class, 'length'],
1656 'argumentCount' => '1',
1657 ],
1658 'LENB' => [
1659 'category' => Category::CATEGORY_TEXT_AND_DATA,
1660 'functionCall' => [TextData\Text::class, 'length'],
1661 'argumentCount' => '1',
1662 ],
1663 'LET' => [
1664 'category' => Category::CATEGORY_LOGICAL,
1665 'functionCall' => [Functions::class, 'DUMMY'],
1666 'argumentCount' => '*',
1667 ],
1668 'LINEST' => [
1669 'category' => Category::CATEGORY_STATISTICAL,
1670 'functionCall' => [Statistical\Trends::class, 'LINEST'],
1671 'argumentCount' => '1-4',
1672 ],
1673 'LN' => [
1674 'category' => Category::CATEGORY_MATH_AND_TRIG,
1675 'functionCall' => [MathTrig\Logarithms::class, 'natural'],
1676 'argumentCount' => '1',
1677 ],
1678 'LOG' => [
1679 'category' => Category::CATEGORY_MATH_AND_TRIG,
1680 'functionCall' => [MathTrig\Logarithms::class, 'withBase'],
1681 'argumentCount' => '1,2',
1682 ],
1683 'LOG10' => [
1684 'category' => Category::CATEGORY_MATH_AND_TRIG,
1685 'functionCall' => [MathTrig\Logarithms::class, 'base10'],
1686 'argumentCount' => '1',
1687 ],
1688 'LOGEST' => [
1689 'category' => Category::CATEGORY_STATISTICAL,
1690 'functionCall' => [Statistical\Trends::class, 'LOGEST'],
1691 'argumentCount' => '1-4',
1692 ],
1693 'LOGINV' => [
1694 'category' => Category::CATEGORY_STATISTICAL,
1695 'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'],
1696 'argumentCount' => '3',
1697 ],
1698 'LOGNORMDIST' => [
1699 'category' => Category::CATEGORY_STATISTICAL,
1700 'functionCall' => [Statistical\Distributions\LogNormal::class, 'cumulative'],
1701 'argumentCount' => '3',
1702 ],
1703 'LOGNORM.DIST' => [
1704 'category' => Category::CATEGORY_STATISTICAL,
1705 'functionCall' => [Statistical\Distributions\LogNormal::class, 'distribution'],
1706 'argumentCount' => '4',
1707 ],
1708 'LOGNORM.INV' => [
1709 'category' => Category::CATEGORY_STATISTICAL,
1710 'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'],
1711 'argumentCount' => '3',
1712 ],
1713 'LOOKUP' => [
1714 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1715 'functionCall' => [LookupRef\Lookup::class, 'lookup'],
1716 'argumentCount' => '2,3',
1717 ],
1718 'LOWER' => [
1719 'category' => Category::CATEGORY_TEXT_AND_DATA,
1720 'functionCall' => [TextData\CaseConvert::class, 'lower'],
1721 'argumentCount' => '1',
1722 ],
1723 'MAKEARRAY' => [
1724 'category' => Category::CATEGORY_LOGICAL,
1725 'functionCall' => [Functions::class, 'DUMMY'],
1726 'argumentCount' => '*',
1727 ],
1728 'MAP' => [
1729 'category' => Category::CATEGORY_LOGICAL,
1730 'functionCall' => [Functions::class, 'DUMMY'],
1731 'argumentCount' => '*',
1732 ],
1733 'MATCH' => [
1734 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
1735 'functionCall' => [LookupRef\ExcelMatch::class, 'MATCH'],
1736 'argumentCount' => '2,3',
1737 ],
1738 'MAX' => [
1739 'category' => Category::CATEGORY_STATISTICAL,
1740 'functionCall' => [Statistical\Maximum::class, 'max'],
1741 'argumentCount' => '1+',
1742 ],
1743 'MAXA' => [
1744 'category' => Category::CATEGORY_STATISTICAL,
1745 'functionCall' => [Statistical\Maximum::class, 'maxA'],
1746 'argumentCount' => '1+',
1747 ],
1748 'MAXIFS' => [
1749 'category' => Category::CATEGORY_STATISTICAL,
1750 'functionCall' => [Statistical\Conditional::class, 'MAXIFS'],
1751 'argumentCount' => '3+',
1752 ],
1753 'MDETERM' => [
1754 'category' => Category::CATEGORY_MATH_AND_TRIG,
1755 'functionCall' => [MathTrig\MatrixFunctions::class, 'determinant'],
1756 'argumentCount' => '1',
1757 ],
1758 'MDURATION' => [
1759 'category' => Category::CATEGORY_FINANCIAL,
1760 'functionCall' => [Functions::class, 'DUMMY'],
1761 'argumentCount' => '5,6',
1762 ],
1763 'MEDIAN' => [
1764 'category' => Category::CATEGORY_STATISTICAL,
1765 'functionCall' => [Statistical\Averages::class, 'median'],
1766 'argumentCount' => '1+',
1767 ],
1768 'MEDIANIF' => [
1769 'category' => Category::CATEGORY_STATISTICAL,
1770 'functionCall' => [Functions::class, 'DUMMY'],
1771 'argumentCount' => '2+',
1772 ],
1773 'MID' => [
1774 'category' => Category::CATEGORY_TEXT_AND_DATA,
1775 'functionCall' => [TextData\Extract::class, 'mid'],
1776 'argumentCount' => '3',
1777 ],
1778 'MIDB' => [
1779 'category' => Category::CATEGORY_TEXT_AND_DATA,
1780 'functionCall' => [TextData\Extract::class, 'mid'],
1781 'argumentCount' => '3',
1782 ],
1783 'MIN' => [
1784 'category' => Category::CATEGORY_STATISTICAL,
1785 'functionCall' => [Statistical\Minimum::class, 'min'],
1786 'argumentCount' => '1+',
1787 ],
1788 'MINA' => [
1789 'category' => Category::CATEGORY_STATISTICAL,
1790 'functionCall' => [Statistical\Minimum::class, 'minA'],
1791 'argumentCount' => '1+',
1792 ],
1793 'MINIFS' => [
1794 'category' => Category::CATEGORY_STATISTICAL,
1795 'functionCall' => [Statistical\Conditional::class, 'MINIFS'],
1796 'argumentCount' => '3+',
1797 ],
1798 'MINUTE' => [
1799 'category' => Category::CATEGORY_DATE_AND_TIME,
1800 'functionCall' => [DateTimeExcel\TimeParts::class, 'minute'],
1801 'argumentCount' => '1',
1802 ],
1803 'MINVERSE' => [
1804 'category' => Category::CATEGORY_MATH_AND_TRIG,
1805 'functionCall' => [MathTrig\MatrixFunctions::class, 'inverse'],
1806 'argumentCount' => '1',
1807 ],
1808 'MIRR' => [
1809 'category' => Category::CATEGORY_FINANCIAL,
1810 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'modifiedRate'],
1811 'argumentCount' => '3',
1812 ],
1813 'MMULT' => [
1814 'category' => Category::CATEGORY_MATH_AND_TRIG,
1815 'functionCall' => [MathTrig\MatrixFunctions::class, 'multiply'],
1816 'argumentCount' => '2',
1817 ],
1818 'MOD' => [
1819 'category' => Category::CATEGORY_MATH_AND_TRIG,
1820 'functionCall' => [MathTrig\Operations::class, 'mod'],
1821 'argumentCount' => '2',
1822 ],
1823 'MODE' => [
1824 'category' => Category::CATEGORY_STATISTICAL,
1825 'functionCall' => [Statistical\Averages::class, 'mode'],
1826 'argumentCount' => '1+',
1827 ],
1828 'MODE.MULT' => [
1829 'category' => Category::CATEGORY_STATISTICAL,
1830 'functionCall' => [Functions::class, 'DUMMY'],
1831 'argumentCount' => '1+',
1832 ],
1833 'MODE.SNGL' => [
1834 'category' => Category::CATEGORY_STATISTICAL,
1835 'functionCall' => [Statistical\Averages::class, 'mode'],
1836 'argumentCount' => '1+',
1837 ],
1838 'MONTH' => [
1839 'category' => Category::CATEGORY_DATE_AND_TIME,
1840 'functionCall' => [DateTimeExcel\DateParts::class, 'month'],
1841 'argumentCount' => '1',
1842 ],
1843 'MROUND' => [
1844 'category' => Category::CATEGORY_MATH_AND_TRIG,
1845 'functionCall' => [MathTrig\Round::class, 'multiple'],
1846 'argumentCount' => '2',
1847 ],
1848 'MULTINOMIAL' => [
1849 'category' => Category::CATEGORY_MATH_AND_TRIG,
1850 'functionCall' => [MathTrig\Factorial::class, 'multinomial'],
1851 'argumentCount' => '1+',
1852 ],
1853 'MUNIT' => [
1854 'category' => Category::CATEGORY_MATH_AND_TRIG,
1855 'functionCall' => [MathTrig\MatrixFunctions::class, 'identity'],
1856 'argumentCount' => '1',
1857 ],
1858 'N' => [
1859 'category' => Category::CATEGORY_INFORMATION,
1860 'functionCall' => [Information\Value::class, 'asNumber'],
1861 'argumentCount' => '1',
1862 ],
1863 'NA' => [
1864 'category' => Category::CATEGORY_INFORMATION,
1865 'functionCall' => [Information\ExcelError::class, 'NA'],
1866 'argumentCount' => '0',
1867 ],
1868 'NEGBINOMDIST' => [
1869 'category' => Category::CATEGORY_STATISTICAL,
1870 'functionCall' => [Statistical\Distributions\Binomial::class, 'negative'],
1871 'argumentCount' => '3',
1872 ],
1873 'NEGBINOM.DIST' => [
1874 'category' => Category::CATEGORY_STATISTICAL,
1875 'functionCall' => [Functions::class, 'DUMMY'],
1876 'argumentCount' => '4',
1877 ],
1878 'NETWORKDAYS' => [
1879 'category' => Category::CATEGORY_DATE_AND_TIME,
1880 'functionCall' => [DateTimeExcel\NetworkDays::class, 'count'],
1881 'argumentCount' => '2-3',
1882 ],
1883 'NETWORKDAYS.INTL' => [
1884 'category' => Category::CATEGORY_DATE_AND_TIME,
1885 'functionCall' => [Functions::class, 'DUMMY'],
1886 'argumentCount' => '2-4',
1887 ],
1888 'NOMINAL' => [
1889 'category' => Category::CATEGORY_FINANCIAL,
1890 'functionCall' => [Financial\InterestRate::class, 'nominal'],
1891 'argumentCount' => '2',
1892 ],
1893 'NORMDIST' => [
1894 'category' => Category::CATEGORY_STATISTICAL,
1895 'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'],
1896 'argumentCount' => '4',
1897 ],
1898 'NORM.DIST' => [
1899 'category' => Category::CATEGORY_STATISTICAL,
1900 'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'],
1901 'argumentCount' => '4',
1902 ],
1903 'NORMINV' => [
1904 'category' => Category::CATEGORY_STATISTICAL,
1905 'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'],
1906 'argumentCount' => '3',
1907 ],
1908 'NORM.INV' => [
1909 'category' => Category::CATEGORY_STATISTICAL,
1910 'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'],
1911 'argumentCount' => '3',
1912 ],
1913 'NORMSDIST' => [
1914 'category' => Category::CATEGORY_STATISTICAL,
1915 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'cumulative'],
1916 'argumentCount' => '1',
1917 ],
1918 'NORM.S.DIST' => [
1919 'category' => Category::CATEGORY_STATISTICAL,
1920 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'distribution'],
1921 'argumentCount' => '1,2',
1922 ],
1923 'NORMSINV' => [
1924 'category' => Category::CATEGORY_STATISTICAL,
1925 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'],
1926 'argumentCount' => '1',
1927 ],
1928 'NORM.S.INV' => [
1929 'category' => Category::CATEGORY_STATISTICAL,
1930 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'],
1931 'argumentCount' => '1',
1932 ],
1933 'NOT' => [
1934 'category' => Category::CATEGORY_LOGICAL,
1935 'functionCall' => [Logical\Operations::class, 'NOT'],
1936 'argumentCount' => '1',
1937 ],
1938 'NOW' => [
1939 'category' => Category::CATEGORY_DATE_AND_TIME,
1940 'functionCall' => [DateTimeExcel\Current::class, 'now'],
1941 'argumentCount' => '0',
1942 ],
1943 'NPER' => [
1944 'category' => Category::CATEGORY_FINANCIAL,
1945 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'periods'],
1946 'argumentCount' => '3-5',
1947 ],
1948 'NPV' => [
1949 'category' => Category::CATEGORY_FINANCIAL,
1950 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'presentValue'],
1951 'argumentCount' => '2+',
1952 ],
1953 'NUMBERSTRING' => [
1954 'category' => Category::CATEGORY_TEXT_AND_DATA,
1955 'functionCall' => [Functions::class, 'DUMMY'],
1956 'argumentCount' => '?',
1957 ],
1958 'NUMBERVALUE' => [
1959 'category' => Category::CATEGORY_TEXT_AND_DATA,
1960 'functionCall' => [TextData\Format::class, 'NUMBERVALUE'],
1961 'argumentCount' => '1+',
1962 ],
1963 'OCT2BIN' => [
1964 'category' => Category::CATEGORY_ENGINEERING,
1965 'functionCall' => [Engineering\ConvertOctal::class, 'toBinary'],
1966 'argumentCount' => '1,2',
1967 ],
1968 'OCT2DEC' => [
1969 'category' => Category::CATEGORY_ENGINEERING,
1970 'functionCall' => [Engineering\ConvertOctal::class, 'toDecimal'],
1971 'argumentCount' => '1',
1972 ],
1973 'OCT2HEX' => [
1974 'category' => Category::CATEGORY_ENGINEERING,
1975 'functionCall' => [Engineering\ConvertOctal::class, 'toHex'],
1976 'argumentCount' => '1,2',
1977 ],
1978 'ODD' => [
1979 'category' => Category::CATEGORY_MATH_AND_TRIG,
1980 'functionCall' => [MathTrig\Round::class, 'odd'],
1981 'argumentCount' => '1',
1982 ],
1983 'ODDFPRICE' => [
1984 'category' => Category::CATEGORY_FINANCIAL,
1985 'functionCall' => [Functions::class, 'DUMMY'],
1986 'argumentCount' => '8,9',
1987 ],
1988 'ODDFYIELD' => [
1989 'category' => Category::CATEGORY_FINANCIAL,
1990 'functionCall' => [Functions::class, 'DUMMY'],
1991 'argumentCount' => '8,9',
1992 ],
1993 'ODDLPRICE' => [
1994 'category' => Category::CATEGORY_FINANCIAL,
1995 'functionCall' => [Functions::class, 'DUMMY'],
1996 'argumentCount' => '7,8',
1997 ],
1998 'ODDLYIELD' => [
1999 'category' => Category::CATEGORY_FINANCIAL,
2000 'functionCall' => [Functions::class, 'DUMMY'],
2001 'argumentCount' => '7,8',
2002 ],
2003 'OFFSET' => [
2004 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2005 'functionCall' => [LookupRef\Offset::class, 'OFFSET'],
2006 'argumentCount' => '3-5',
2007 'passCellReference' => true,
2008 'passByReference' => [true],
2009 ],
2010 'OR' => [
2011 'category' => Category::CATEGORY_LOGICAL,
2012 'functionCall' => [Logical\Operations::class, 'logicalOr'],
2013 'argumentCount' => '1+',
2014 ],
2015 'PDURATION' => [
2016 'category' => Category::CATEGORY_FINANCIAL,
2017 'functionCall' => [Financial\CashFlow\Single::class, 'periods'],
2018 'argumentCount' => '3',
2019 ],
2020 'PEARSON' => [
2021 'category' => Category::CATEGORY_STATISTICAL,
2022 'functionCall' => [Statistical\Trends::class, 'CORREL'],
2023 'argumentCount' => '2',
2024 ],
2025 'PERCENTILE' => [
2026 'category' => Category::CATEGORY_STATISTICAL,
2027 'functionCall' => [Statistical\Percentiles::class, 'PERCENTILE'],
2028 'argumentCount' => '2',
2029 ],
2030 'PERCENTILE.EXC' => [
2031 'category' => Category::CATEGORY_STATISTICAL,
2032 'functionCall' => [Functions::class, 'DUMMY'],
2033 'argumentCount' => '2',
2034 ],
2035 'PERCENTILE.INC' => [
2036 'category' => Category::CATEGORY_STATISTICAL,
2037 'functionCall' => [Statistical\Percentiles::class, 'PERCENTILE'],
2038 'argumentCount' => '2',
2039 ],
2040 'PERCENTRANK' => [
2041 'category' => Category::CATEGORY_STATISTICAL,
2042 'functionCall' => [Statistical\Percentiles::class, 'PERCENTRANK'],
2043 'argumentCount' => '2,3',
2044 ],
2045 'PERCENTRANK.EXC' => [
2046 'category' => Category::CATEGORY_STATISTICAL,
2047 'functionCall' => [Functions::class, 'DUMMY'],
2048 'argumentCount' => '2,3',
2049 ],
2050 'PERCENTRANK.INC' => [
2051 'category' => Category::CATEGORY_STATISTICAL,
2052 'functionCall' => [Statistical\Percentiles::class, 'PERCENTRANK'],
2053 'argumentCount' => '2,3',
2054 ],
2055 'PERMUT' => [
2056 'category' => Category::CATEGORY_STATISTICAL,
2057 'functionCall' => [Statistical\Permutations::class, 'PERMUT'],
2058 'argumentCount' => '2',
2059 ],
2060 'PERMUTATIONA' => [
2061 'category' => Category::CATEGORY_STATISTICAL,
2062 'functionCall' => [Statistical\Permutations::class, 'PERMUTATIONA'],
2063 'argumentCount' => '2',
2064 ],
2065 'PHONETIC' => [
2066 'category' => Category::CATEGORY_TEXT_AND_DATA,
2067 'functionCall' => [Functions::class, 'DUMMY'],
2068 'argumentCount' => '1',
2069 ],
2070 'PHI' => [
2071 'category' => Category::CATEGORY_STATISTICAL,
2072 'functionCall' => [Functions::class, 'DUMMY'],
2073 'argumentCount' => '1',
2074 ],
2075 'PI' => [
2076 'category' => Category::CATEGORY_MATH_AND_TRIG,
2077 'functionCall' => 'pi',
2078 'argumentCount' => '0',
2079 ],
2080 'PMT' => [
2081 'category' => Category::CATEGORY_FINANCIAL,
2082 'functionCall' => [Financial\CashFlow\Constant\Periodic\Payments::class, 'annuity'],
2083 'argumentCount' => '3-5',
2084 ],
2085 'POISSON' => [
2086 'category' => Category::CATEGORY_STATISTICAL,
2087 'functionCall' => [Statistical\Distributions\Poisson::class, 'distribution'],
2088 'argumentCount' => '3',
2089 ],
2090 'POISSON.DIST' => [
2091 'category' => Category::CATEGORY_STATISTICAL,
2092 'functionCall' => [Statistical\Distributions\Poisson::class, 'distribution'],
2093 'argumentCount' => '3',
2094 ],
2095 'POWER' => [
2096 'category' => Category::CATEGORY_MATH_AND_TRIG,
2097 'functionCall' => [MathTrig\Operations::class, 'power'],
2098 'argumentCount' => '2',
2099 ],
2100 'PPMT' => [
2101 'category' => Category::CATEGORY_FINANCIAL,
2102 'functionCall' => [Financial\CashFlow\Constant\Periodic\Payments::class, 'interestPayment'],
2103 'argumentCount' => '4-6',
2104 ],
2105 'PRICE' => [
2106 'category' => Category::CATEGORY_FINANCIAL,
2107 'functionCall' => [Financial\Securities\Price::class, 'price'],
2108 'argumentCount' => '6,7',
2109 ],
2110 'PRICEDISC' => [
2111 'category' => Category::CATEGORY_FINANCIAL,
2112 'functionCall' => [Financial\Securities\Price::class, 'priceDiscounted'],
2113 'argumentCount' => '4,5',
2114 ],
2115 'PRICEMAT' => [
2116 'category' => Category::CATEGORY_FINANCIAL,
2117 'functionCall' => [Financial\Securities\Price::class, 'priceAtMaturity'],
2118 'argumentCount' => '5,6',
2119 ],
2120 'PROB' => [
2121 'category' => Category::CATEGORY_STATISTICAL,
2122 'functionCall' => [Functions::class, 'DUMMY'],
2123 'argumentCount' => '3,4',
2124 ],
2125 'PRODUCT' => [
2126 'category' => Category::CATEGORY_MATH_AND_TRIG,
2127 'functionCall' => [MathTrig\Operations::class, 'product'],
2128 'argumentCount' => '1+',
2129 ],
2130 'PROPER' => [
2131 'category' => Category::CATEGORY_TEXT_AND_DATA,
2132 'functionCall' => [TextData\CaseConvert::class, 'proper'],
2133 'argumentCount' => '1',
2134 ],
2135 'PV' => [
2136 'category' => Category::CATEGORY_FINANCIAL,
2137 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'presentValue'],
2138 'argumentCount' => '3-5',
2139 ],
2140 'QUARTILE' => [
2141 'category' => Category::CATEGORY_STATISTICAL,
2142 'functionCall' => [Statistical\Percentiles::class, 'QUARTILE'],
2143 'argumentCount' => '2',
2144 ],
2145 'QUARTILE.EXC' => [
2146 'category' => Category::CATEGORY_STATISTICAL,
2147 'functionCall' => [Functions::class, 'DUMMY'],
2148 'argumentCount' => '2',
2149 ],
2150 'QUARTILE.INC' => [
2151 'category' => Category::CATEGORY_STATISTICAL,
2152 'functionCall' => [Statistical\Percentiles::class, 'QUARTILE'],
2153 'argumentCount' => '2',
2154 ],
2155 'QUOTIENT' => [
2156 'category' => Category::CATEGORY_MATH_AND_TRIG,
2157 'functionCall' => [MathTrig\Operations::class, 'quotient'],
2158 'argumentCount' => '2',
2159 ],
2160 'RADIANS' => [
2161 'category' => Category::CATEGORY_MATH_AND_TRIG,
2162 'functionCall' => [MathTrig\Angle::class, 'toRadians'],
2163 'argumentCount' => '1',
2164 ],
2165 'RAND' => [
2166 'category' => Category::CATEGORY_MATH_AND_TRIG,
2167 'functionCall' => [MathTrig\Random::class, 'rand'],
2168 'argumentCount' => '0',
2169 ],
2170 'RANDARRAY' => [
2171 'category' => Category::CATEGORY_MATH_AND_TRIG,
2172 'functionCall' => [MathTrig\Random::class, 'randArray'],
2173 'argumentCount' => '0-5',
2174 ],
2175 'RANDBETWEEN' => [
2176 'category' => Category::CATEGORY_MATH_AND_TRIG,
2177 'functionCall' => [MathTrig\Random::class, 'randBetween'],
2178 'argumentCount' => '2',
2179 ],
2180 'RANK' => [
2181 'category' => Category::CATEGORY_STATISTICAL,
2182 'functionCall' => [Statistical\Percentiles::class, 'RANK'],
2183 'argumentCount' => '2,3',
2184 ],
2185 'RANK.AVG' => [
2186 'category' => Category::CATEGORY_STATISTICAL,
2187 'functionCall' => [Functions::class, 'DUMMY'],
2188 'argumentCount' => '2,3',
2189 ],
2190 'RANK.EQ' => [
2191 'category' => Category::CATEGORY_STATISTICAL,
2192 'functionCall' => [Statistical\Percentiles::class, 'RANK'],
2193 'argumentCount' => '2,3',
2194 ],
2195 'RATE' => [
2196 'category' => Category::CATEGORY_FINANCIAL,
2197 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'rate'],
2198 'argumentCount' => '3-6',
2199 ],
2200 'RECEIVED' => [
2201 'category' => Category::CATEGORY_FINANCIAL,
2202 'functionCall' => [Financial\Securities\Price::class, 'received'],
2203 'argumentCount' => '4-5',
2204 ],
2205 'REDUCE' => [
2206 'category' => Category::CATEGORY_LOGICAL,
2207 'functionCall' => [Functions::class, 'DUMMY'],
2208 'argumentCount' => '*',
2209 ],
2210 'REPLACE' => [
2211 'category' => Category::CATEGORY_TEXT_AND_DATA,
2212 'functionCall' => [TextData\Replace::class, 'replace'],
2213 'argumentCount' => '4',
2214 ],
2215 'REPLACEB' => [
2216 'category' => Category::CATEGORY_TEXT_AND_DATA,
2217 'functionCall' => [TextData\Replace::class, 'replace'],
2218 'argumentCount' => '4',
2219 ],
2220 'REPT' => [
2221 'category' => Category::CATEGORY_TEXT_AND_DATA,
2222 'functionCall' => [TextData\Concatenate::class, 'builtinREPT'],
2223 'argumentCount' => '2',
2224 ],
2225 'RIGHT' => [
2226 'category' => Category::CATEGORY_TEXT_AND_DATA,
2227 'functionCall' => [TextData\Extract::class, 'right'],
2228 'argumentCount' => '1,2',
2229 ],
2230 'RIGHTB' => [
2231 'category' => Category::CATEGORY_TEXT_AND_DATA,
2232 'functionCall' => [TextData\Extract::class, 'right'],
2233 'argumentCount' => '1,2',
2234 ],
2235 'ROMAN' => [
2236 'category' => Category::CATEGORY_MATH_AND_TRIG,
2237 'functionCall' => [MathTrig\Roman::class, 'evaluate'],
2238 'argumentCount' => '1,2',
2239 ],
2240 'ROUND' => [
2241 'category' => Category::CATEGORY_MATH_AND_TRIG,
2242 'functionCall' => [MathTrig\Round::class, 'round'],
2243 'argumentCount' => '2',
2244 ],
2245 'ROUNDBAHTDOWN' => [
2246 'category' => Category::CATEGORY_MATH_AND_TRIG,
2247 'functionCall' => [Functions::class, 'DUMMY'],
2248 'argumentCount' => '?',
2249 ],
2250 'ROUNDBAHTUP' => [
2251 'category' => Category::CATEGORY_MATH_AND_TRIG,
2252 'functionCall' => [Functions::class, 'DUMMY'],
2253 'argumentCount' => '?',
2254 ],
2255 'ROUNDDOWN' => [
2256 'category' => Category::CATEGORY_MATH_AND_TRIG,
2257 'functionCall' => [MathTrig\Round::class, 'down'],
2258 'argumentCount' => '2',
2259 ],
2260 'ROUNDUP' => [
2261 'category' => Category::CATEGORY_MATH_AND_TRIG,
2262 'functionCall' => [MathTrig\Round::class, 'up'],
2263 'argumentCount' => '2',
2264 ],
2265 'ROW' => [
2266 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2267 'functionCall' => [LookupRef\RowColumnInformation::class, 'ROW'],
2268 'argumentCount' => '-1',
2269 'passCellReference' => true,
2270 'passByReference' => [true],
2271 ],
2272 'ROWS' => [
2273 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2274 'functionCall' => [LookupRef\RowColumnInformation::class, 'ROWS'],
2275 'argumentCount' => '1',
2276 ],
2277 'RRI' => [
2278 'category' => Category::CATEGORY_FINANCIAL,
2279 'functionCall' => [Financial\CashFlow\Single::class, 'interestRate'],
2280 'argumentCount' => '3',
2281 ],
2282 'RSQ' => [
2283 'category' => Category::CATEGORY_STATISTICAL,
2284 'functionCall' => [Statistical\Trends::class, 'RSQ'],
2285 'argumentCount' => '2',
2286 ],
2287 'RTD' => [
2288 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2289 'functionCall' => [Functions::class, 'DUMMY'],
2290 'argumentCount' => '1+',
2291 ],
2292 'SEARCH' => [
2293 'category' => Category::CATEGORY_TEXT_AND_DATA,
2294 'functionCall' => [TextData\Search::class, 'insensitive'],
2295 'argumentCount' => '2,3',
2296 ],
2297 'SCAN' => [
2298 'category' => Category::CATEGORY_LOGICAL,
2299 'functionCall' => [Functions::class, 'DUMMY'],
2300 'argumentCount' => '*',
2301 ],
2302 'SEARCHB' => [
2303 'category' => Category::CATEGORY_TEXT_AND_DATA,
2304 'functionCall' => [TextData\Search::class, 'insensitive'],
2305 'argumentCount' => '2,3',
2306 ],
2307 'SEC' => [
2308 'category' => Category::CATEGORY_MATH_AND_TRIG,
2309 'functionCall' => [MathTrig\Trig\Secant::class, 'sec'],
2310 'argumentCount' => '1',
2311 ],
2312 'SECH' => [
2313 'category' => Category::CATEGORY_MATH_AND_TRIG,
2314 'functionCall' => [MathTrig\Trig\Secant::class, 'sech'],
2315 'argumentCount' => '1',
2316 ],
2317 'SECOND' => [
2318 'category' => Category::CATEGORY_DATE_AND_TIME,
2319 'functionCall' => [DateTimeExcel\TimeParts::class, 'second'],
2320 'argumentCount' => '1',
2321 ],
2322 'SEQUENCE' => [
2323 'category' => Category::CATEGORY_MATH_AND_TRIG,
2324 'functionCall' => [MathTrig\MatrixFunctions::class, 'sequence'],
2325 'argumentCount' => '1-4',
2326 ],
2327 'SERIESSUM' => [
2328 'category' => Category::CATEGORY_MATH_AND_TRIG,
2329 'functionCall' => [MathTrig\SeriesSum::class, 'evaluate'],
2330 'argumentCount' => '4',
2331 ],
2332 'SHEET' => [
2333 'category' => Category::CATEGORY_INFORMATION,
2334 'functionCall' => [Functions::class, 'DUMMY'],
2335 'argumentCount' => '0,1',
2336 ],
2337 'SHEETS' => [
2338 'category' => Category::CATEGORY_INFORMATION,
2339 'functionCall' => [Functions::class, 'DUMMY'],
2340 'argumentCount' => '0,1',
2341 ],
2342 'SIGN' => [
2343 'category' => Category::CATEGORY_MATH_AND_TRIG,
2344 'functionCall' => [MathTrig\Sign::class, 'evaluate'],
2345 'argumentCount' => '1',
2346 ],
2347 'SIN' => [
2348 'category' => Category::CATEGORY_MATH_AND_TRIG,
2349 'functionCall' => [MathTrig\Trig\Sine::class, 'sin'],
2350 'argumentCount' => '1',
2351 ],
2352 'SINGLE' => [
2353 'category' => Category::CATEGORY_UNCATEGORISED,
2354 'functionCall' => [Functions::class, 'DUMMY'],
2355 'argumentCount' => '*',
2356 ],
2357 'SINH' => [
2358 'category' => Category::CATEGORY_MATH_AND_TRIG,
2359 'functionCall' => [MathTrig\Trig\Sine::class, 'sinh'],
2360 'argumentCount' => '1',
2361 ],
2362 'SKEW' => [
2363 'category' => Category::CATEGORY_STATISTICAL,
2364 'functionCall' => [Statistical\Deviations::class, 'skew'],
2365 'argumentCount' => '1+',
2366 ],
2367 'SKEW.P' => [
2368 'category' => Category::CATEGORY_STATISTICAL,
2369 'functionCall' => [Functions::class, 'DUMMY'],
2370 'argumentCount' => '1+',
2371 ],
2372 'SLN' => [
2373 'category' => Category::CATEGORY_FINANCIAL,
2374 'functionCall' => [Financial\Depreciation::class, 'SLN'],
2375 'argumentCount' => '3',
2376 ],
2377 'SLOPE' => [
2378 'category' => Category::CATEGORY_STATISTICAL,
2379 'functionCall' => [Statistical\Trends::class, 'SLOPE'],
2380 'argumentCount' => '2',
2381 ],
2382 'SMALL' => [
2383 'category' => Category::CATEGORY_STATISTICAL,
2384 'functionCall' => [Statistical\Size::class, 'small'],
2385 'argumentCount' => '2',
2386 ],
2387 'SORT' => [
2388 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2389 'functionCall' => [LookupRef\Sort::class, 'sort'],
2390 'argumentCount' => '1-4',
2391 ],
2392 'SORTBY' => [
2393 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2394 'functionCall' => [LookupRef\Sort::class, 'sortBy'],
2395 'argumentCount' => '2+',
2396 ],
2397 'SQRT' => [
2398 'category' => Category::CATEGORY_MATH_AND_TRIG,
2399 'functionCall' => [MathTrig\Sqrt::class, 'sqrt'],
2400 'argumentCount' => '1',
2401 ],
2402 'SQRTPI' => [
2403 'category' => Category::CATEGORY_MATH_AND_TRIG,
2404 'functionCall' => [MathTrig\Sqrt::class, 'pi'],
2405 'argumentCount' => '1',
2406 ],
2407 'STANDARDIZE' => [
2408 'category' => Category::CATEGORY_STATISTICAL,
2409 'functionCall' => [Statistical\Standardize::class, 'execute'],
2410 'argumentCount' => '3',
2411 ],
2412 'STDEV' => [
2413 'category' => Category::CATEGORY_STATISTICAL,
2414 'functionCall' => [Statistical\StandardDeviations::class, 'STDEV'],
2415 'argumentCount' => '1+',
2416 ],
2417 'STDEV.S' => [
2418 'category' => Category::CATEGORY_STATISTICAL,
2419 'functionCall' => [Statistical\StandardDeviations::class, 'STDEV'],
2420 'argumentCount' => '1+',
2421 ],
2422 'STDEV.P' => [
2423 'category' => Category::CATEGORY_STATISTICAL,
2424 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVP'],
2425 'argumentCount' => '1+',
2426 ],
2427 'STDEVA' => [
2428 'category' => Category::CATEGORY_STATISTICAL,
2429 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVA'],
2430 'argumentCount' => '1+',
2431 ],
2432 'STDEVP' => [
2433 'category' => Category::CATEGORY_STATISTICAL,
2434 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVP'],
2435 'argumentCount' => '1+',
2436 ],
2437 'STDEVPA' => [
2438 'category' => Category::CATEGORY_STATISTICAL,
2439 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVPA'],
2440 'argumentCount' => '1+',
2441 ],
2442 'STEYX' => [
2443 'category' => Category::CATEGORY_STATISTICAL,
2444 'functionCall' => [Statistical\Trends::class, 'STEYX'],
2445 'argumentCount' => '2',
2446 ],
2447 'SUBSTITUTE' => [
2448 'category' => Category::CATEGORY_TEXT_AND_DATA,
2449 'functionCall' => [TextData\Replace::class, 'substitute'],
2450 'argumentCount' => '3,4',
2451 ],
2452 'SUBTOTAL' => [
2453 'category' => Category::CATEGORY_MATH_AND_TRIG,
2454 'functionCall' => [MathTrig\Subtotal::class, 'evaluate'],
2455 'argumentCount' => '2+',
2456 'passCellReference' => true,
2457 ],
2458 'SUM' => [
2459 'category' => Category::CATEGORY_MATH_AND_TRIG,
2460 'functionCall' => [MathTrig\Sum::class, 'sumErroringStrings'],
2461 'argumentCount' => '1+',
2462 ],
2463 'SUMIF' => [
2464 'category' => Category::CATEGORY_MATH_AND_TRIG,
2465 'functionCall' => [Statistical\Conditional::class, 'SUMIF'],
2466 'argumentCount' => '2,3',
2467 ],
2468 'SUMIFS' => [
2469 'category' => Category::CATEGORY_MATH_AND_TRIG,
2470 'functionCall' => [Statistical\Conditional::class, 'SUMIFS'],
2471 'argumentCount' => '3+',
2472 ],
2473 'SUMPRODUCT' => [
2474 'category' => Category::CATEGORY_MATH_AND_TRIG,
2475 'functionCall' => [MathTrig\Sum::class, 'product'],
2476 'argumentCount' => '1+',
2477 ],
2478 'SUMSQ' => [
2479 'category' => Category::CATEGORY_MATH_AND_TRIG,
2480 'functionCall' => [MathTrig\SumSquares::class, 'sumSquare'],
2481 'argumentCount' => '1+',
2482 ],
2483 'SUMX2MY2' => [
2484 'category' => Category::CATEGORY_MATH_AND_TRIG,
2485 'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredMinusYSquared'],
2486 'argumentCount' => '2',
2487 ],
2488 'SUMX2PY2' => [
2489 'category' => Category::CATEGORY_MATH_AND_TRIG,
2490 'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredPlusYSquared'],
2491 'argumentCount' => '2',
2492 ],
2493 'SUMXMY2' => [
2494 'category' => Category::CATEGORY_MATH_AND_TRIG,
2495 'functionCall' => [MathTrig\SumSquares::class, 'sumXMinusYSquared'],
2496 'argumentCount' => '2',
2497 ],
2498 'SWITCH' => [
2499 'category' => Category::CATEGORY_LOGICAL,
2500 'functionCall' => [Logical\Conditional::class, 'statementSwitch'],
2501 'argumentCount' => '3+',
2502 ],
2503 'SYD' => [
2504 'category' => Category::CATEGORY_FINANCIAL,
2505 'functionCall' => [Financial\Depreciation::class, 'SYD'],
2506 'argumentCount' => '4',
2507 ],
2508 'T' => [
2509 'category' => Category::CATEGORY_TEXT_AND_DATA,
2510 'functionCall' => [TextData\Text::class, 'test'],
2511 'argumentCount' => '1',
2512 ],
2513 'TAKE' => [
2514 'category' => Category::CATEGORY_MATH_AND_TRIG,
2515 'functionCall' => [Functions::class, 'DUMMY'],
2516 'argumentCount' => '2-3',
2517 ],
2518 'TAN' => [
2519 'category' => Category::CATEGORY_MATH_AND_TRIG,
2520 'functionCall' => [MathTrig\Trig\Tangent::class, 'tan'],
2521 'argumentCount' => '1',
2522 ],
2523 'TANH' => [
2524 'category' => Category::CATEGORY_MATH_AND_TRIG,
2525 'functionCall' => [MathTrig\Trig\Tangent::class, 'tanh'],
2526 'argumentCount' => '1',
2527 ],
2528 'TBILLEQ' => [
2529 'category' => Category::CATEGORY_FINANCIAL,
2530 'functionCall' => [Financial\TreasuryBill::class, 'bondEquivalentYield'],
2531 'argumentCount' => '3',
2532 ],
2533 'TBILLPRICE' => [
2534 'category' => Category::CATEGORY_FINANCIAL,
2535 'functionCall' => [Financial\TreasuryBill::class, 'price'],
2536 'argumentCount' => '3',
2537 ],
2538 'TBILLYIELD' => [
2539 'category' => Category::CATEGORY_FINANCIAL,
2540 'functionCall' => [Financial\TreasuryBill::class, 'yield'],
2541 'argumentCount' => '3',
2542 ],
2543 'TDIST' => [
2544 'category' => Category::CATEGORY_STATISTICAL,
2545 'functionCall' => [Statistical\Distributions\StudentT::class, 'distribution'],
2546 'argumentCount' => '3',
2547 ],
2548 'T.DIST' => [
2549 'category' => Category::CATEGORY_STATISTICAL,
2550 'functionCall' => [Functions::class, 'DUMMY'],
2551 'argumentCount' => '3',
2552 ],
2553 'T.DIST.2T' => [
2554 'category' => Category::CATEGORY_STATISTICAL,
2555 'functionCall' => [Functions::class, 'DUMMY'],
2556 'argumentCount' => '2',
2557 ],
2558 'T.DIST.RT' => [
2559 'category' => Category::CATEGORY_STATISTICAL,
2560 'functionCall' => [Functions::class, 'DUMMY'],
2561 'argumentCount' => '2',
2562 ],
2563 'TEXT' => [
2564 'category' => Category::CATEGORY_TEXT_AND_DATA,
2565 'functionCall' => [TextData\Format::class, 'TEXTFORMAT'],
2566 'argumentCount' => '2',
2567 ],
2568 'TEXTAFTER' => [
2569 'category' => Category::CATEGORY_TEXT_AND_DATA,
2570 'functionCall' => [TextData\Extract::class, 'after'],
2571 'argumentCount' => '2-6',
2572 ],
2573 'TEXTBEFORE' => [
2574 'category' => Category::CATEGORY_TEXT_AND_DATA,
2575 'functionCall' => [TextData\Extract::class, 'before'],
2576 'argumentCount' => '2-6',
2577 ],
2578 'TEXTJOIN' => [
2579 'category' => Category::CATEGORY_TEXT_AND_DATA,
2580 'functionCall' => [TextData\Concatenate::class, 'TEXTJOIN'],
2581 'argumentCount' => '3+',
2582 ],
2583 'TEXTSPLIT' => [
2584 'category' => Category::CATEGORY_TEXT_AND_DATA,
2585 'functionCall' => [TextData\Text::class, 'split'],
2586 'argumentCount' => '2-6',
2587 ],
2588 'THAIDAYOFWEEK' => [
2589 'category' => Category::CATEGORY_DATE_AND_TIME,
2590 'functionCall' => [Functions::class, 'DUMMY'],
2591 'argumentCount' => '?',
2592 ],
2593 'THAIDIGIT' => [
2594 'category' => Category::CATEGORY_TEXT_AND_DATA,
2595 'functionCall' => [Functions::class, 'DUMMY'],
2596 'argumentCount' => '?',
2597 ],
2598 'THAIMONTHOFYEAR' => [
2599 'category' => Category::CATEGORY_DATE_AND_TIME,
2600 'functionCall' => [Functions::class, 'DUMMY'],
2601 'argumentCount' => '?',
2602 ],
2603 'THAINUMSOUND' => [
2604 'category' => Category::CATEGORY_TEXT_AND_DATA,
2605 'functionCall' => [Functions::class, 'DUMMY'],
2606 'argumentCount' => '?',
2607 ],
2608 'THAINUMSTRING' => [
2609 'category' => Category::CATEGORY_TEXT_AND_DATA,
2610 'functionCall' => [Functions::class, 'DUMMY'],
2611 'argumentCount' => '?',
2612 ],
2613 'THAISTRINGLENGTH' => [
2614 'category' => Category::CATEGORY_TEXT_AND_DATA,
2615 'functionCall' => [Functions::class, 'DUMMY'],
2616 'argumentCount' => '?',
2617 ],
2618 'THAIYEAR' => [
2619 'category' => Category::CATEGORY_DATE_AND_TIME,
2620 'functionCall' => [Functions::class, 'DUMMY'],
2621 'argumentCount' => '?',
2622 ],
2623 'TIME' => [
2624 'category' => Category::CATEGORY_DATE_AND_TIME,
2625 'functionCall' => [DateTimeExcel\Time::class, 'fromHMS'],
2626 'argumentCount' => '3',
2627 ],
2628 'TIMEVALUE' => [
2629 'category' => Category::CATEGORY_DATE_AND_TIME,
2630 'functionCall' => [DateTimeExcel\TimeValue::class, 'fromString'],
2631 'argumentCount' => '1',
2632 ],
2633 'TINV' => [
2634 'category' => Category::CATEGORY_STATISTICAL,
2635 'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
2636 'argumentCount' => '2',
2637 ],
2638 'T.INV' => [
2639 'category' => Category::CATEGORY_STATISTICAL,
2640 'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
2641 'argumentCount' => '2',
2642 ],
2643 'T.INV.2T' => [
2644 'category' => Category::CATEGORY_STATISTICAL,
2645 'functionCall' => [Functions::class, 'DUMMY'],
2646 'argumentCount' => '2',
2647 ],
2648 'TODAY' => [
2649 'category' => Category::CATEGORY_DATE_AND_TIME,
2650 'functionCall' => [DateTimeExcel\Current::class, 'today'],
2651 'argumentCount' => '0',
2652 ],
2653 'TOCOL' => [
2654 'category' => Category::CATEGORY_MATH_AND_TRIG,
2655 'functionCall' => [Functions::class, 'DUMMY'],
2656 'argumentCount' => '1-3',
2657 ],
2658 'TOROW' => [
2659 'category' => Category::CATEGORY_MATH_AND_TRIG,
2660 'functionCall' => [Functions::class, 'DUMMY'],
2661 'argumentCount' => '1-3',
2662 ],
2663 'TRANSPOSE' => [
2664 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2665 'functionCall' => [LookupRef\Matrix::class, 'transpose'],
2666 'argumentCount' => '1',
2667 ],
2668 'TREND' => [
2669 'category' => Category::CATEGORY_STATISTICAL,
2670 'functionCall' => [Statistical\Trends::class, 'TREND'],
2671 'argumentCount' => '1-4',
2672 ],
2673 'TRIM' => [
2674 'category' => Category::CATEGORY_TEXT_AND_DATA,
2675 'functionCall' => [TextData\Trim::class, 'spaces'],
2676 'argumentCount' => '1',
2677 ],
2678 'TRIMMEAN' => [
2679 'category' => Category::CATEGORY_STATISTICAL,
2680 'functionCall' => [Statistical\Averages\Mean::class, 'trim'],
2681 'argumentCount' => '2',
2682 ],
2683 'TRUE' => [
2684 'category' => Category::CATEGORY_LOGICAL,
2685 'functionCall' => [Logical\Boolean::class, 'TRUE'],
2686 'argumentCount' => '0',
2687 ],
2688 'TRUNC' => [
2689 'category' => Category::CATEGORY_MATH_AND_TRIG,
2690 'functionCall' => [MathTrig\Trunc::class, 'evaluate'],
2691 'argumentCount' => '1,2',
2692 ],
2693 'TTEST' => [
2694 'category' => Category::CATEGORY_STATISTICAL,
2695 'functionCall' => [Functions::class, 'DUMMY'],
2696 'argumentCount' => '4',
2697 ],
2698 'T.TEST' => [
2699 'category' => Category::CATEGORY_STATISTICAL,
2700 'functionCall' => [Functions::class, 'DUMMY'],
2701 'argumentCount' => '4',
2702 ],
2703 'TYPE' => [
2704 'category' => Category::CATEGORY_INFORMATION,
2705 'functionCall' => [Information\Value::class, 'type'],
2706 'argumentCount' => '1',
2707 ],
2708 'UNICHAR' => [
2709 'category' => Category::CATEGORY_TEXT_AND_DATA,
2710 'functionCall' => [TextData\CharacterConvert::class, 'character'],
2711 'argumentCount' => '1',
2712 ],
2713 'UNICODE' => [
2714 'category' => Category::CATEGORY_TEXT_AND_DATA,
2715 'functionCall' => [TextData\CharacterConvert::class, 'code'],
2716 'argumentCount' => '1',
2717 ],
2718 'UNIQUE' => [
2719 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2720 'functionCall' => [LookupRef\Unique::class, 'unique'],
2721 'argumentCount' => '1+',
2722 ],
2723 'UPPER' => [
2724 'category' => Category::CATEGORY_TEXT_AND_DATA,
2725 'functionCall' => [TextData\CaseConvert::class, 'upper'],
2726 'argumentCount' => '1',
2727 ],
2728 'USDOLLAR' => [
2729 'category' => Category::CATEGORY_FINANCIAL,
2730 'functionCall' => [Financial\Dollar::class, 'format'],
2731 'argumentCount' => '2',
2732 ],
2733 'VALUE' => [
2734 'category' => Category::CATEGORY_TEXT_AND_DATA,
2735 'functionCall' => [TextData\Format::class, 'VALUE'],
2736 'argumentCount' => '1',
2737 ],
2738 'VALUETOTEXT' => [
2739 'category' => Category::CATEGORY_TEXT_AND_DATA,
2740 'functionCall' => [TextData\Format::class, 'valueToText'],
2741 'argumentCount' => '1,2',
2742 ],
2743 'VAR' => [
2744 'category' => Category::CATEGORY_STATISTICAL,
2745 'functionCall' => [Statistical\Variances::class, 'VAR'],
2746 'argumentCount' => '1+',
2747 ],
2748 'VAR.P' => [
2749 'category' => Category::CATEGORY_STATISTICAL,
2750 'functionCall' => [Statistical\Variances::class, 'VARP'],
2751 'argumentCount' => '1+',
2752 ],
2753 'VAR.S' => [
2754 'category' => Category::CATEGORY_STATISTICAL,
2755 'functionCall' => [Statistical\Variances::class, 'VAR'],
2756 'argumentCount' => '1+',
2757 ],
2758 'VARA' => [
2759 'category' => Category::CATEGORY_STATISTICAL,
2760 'functionCall' => [Statistical\Variances::class, 'VARA'],
2761 'argumentCount' => '1+',
2762 ],
2763 'VARP' => [
2764 'category' => Category::CATEGORY_STATISTICAL,
2765 'functionCall' => [Statistical\Variances::class, 'VARP'],
2766 'argumentCount' => '1+',
2767 ],
2768 'VARPA' => [
2769 'category' => Category::CATEGORY_STATISTICAL,
2770 'functionCall' => [Statistical\Variances::class, 'VARPA'],
2771 'argumentCount' => '1+',
2772 ],
2773 'VDB' => [
2774 'category' => Category::CATEGORY_FINANCIAL,
2775 'functionCall' => [Functions::class, 'DUMMY'],
2776 'argumentCount' => '5-7',
2777 ],
2778 'VLOOKUP' => [
2779 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2780 'functionCall' => [LookupRef\VLookup::class, 'lookup'],
2781 'argumentCount' => '3,4',
2782 ],
2783 'VSTACK' => [
2784 'category' => Category::CATEGORY_MATH_AND_TRIG,
2785 'functionCall' => [Functions::class, 'DUMMY'],
2786 'argumentCount' => '1+',
2787 ],
2788 'WEBSERVICE' => [
2789 'category' => Category::CATEGORY_WEB,
2790 'functionCall' => [Web\Service::class, 'webService'],
2791 'argumentCount' => '1',
2792 ],
2793 'WEEKDAY' => [
2794 'category' => Category::CATEGORY_DATE_AND_TIME,
2795 'functionCall' => [DateTimeExcel\Week::class, 'day'],
2796 'argumentCount' => '1,2',
2797 ],
2798 'WEEKNUM' => [
2799 'category' => Category::CATEGORY_DATE_AND_TIME,
2800 'functionCall' => [DateTimeExcel\Week::class, 'number'],
2801 'argumentCount' => '1,2',
2802 ],
2803 'WEIBULL' => [
2804 'category' => Category::CATEGORY_STATISTICAL,
2805 'functionCall' => [Statistical\Distributions\Weibull::class, 'distribution'],
2806 'argumentCount' => '4',
2807 ],
2808 'WEIBULL.DIST' => [
2809 'category' => Category::CATEGORY_STATISTICAL,
2810 'functionCall' => [Statistical\Distributions\Weibull::class, 'distribution'],
2811 'argumentCount' => '4',
2812 ],
2813 'WORKDAY' => [
2814 'category' => Category::CATEGORY_DATE_AND_TIME,
2815 'functionCall' => [DateTimeExcel\WorkDay::class, 'date'],
2816 'argumentCount' => '2-3',
2817 ],
2818 'WORKDAY.INTL' => [
2819 'category' => Category::CATEGORY_DATE_AND_TIME,
2820 'functionCall' => [Functions::class, 'DUMMY'],
2821 'argumentCount' => '2-4',
2822 ],
2823 'WRAPCOLS' => [
2824 'category' => Category::CATEGORY_MATH_AND_TRIG,
2825 'functionCall' => [Functions::class, 'DUMMY'],
2826 'argumentCount' => '2-3',
2827 ],
2828 'WRAPROWS' => [
2829 'category' => Category::CATEGORY_MATH_AND_TRIG,
2830 'functionCall' => [Functions::class, 'DUMMY'],
2831 'argumentCount' => '2-3',
2832 ],
2833 'XIRR' => [
2834 'category' => Category::CATEGORY_FINANCIAL,
2835 'functionCall' => [Financial\CashFlow\Variable\NonPeriodic::class, 'rate'],
2836 'argumentCount' => '2,3',
2837 ],
2838 'XLOOKUP' => [
2839 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2840 'functionCall' => [Functions::class, 'DUMMY'],
2841 'argumentCount' => '3-6',
2842 ],
2843 'XNPV' => [
2844 'category' => Category::CATEGORY_FINANCIAL,
2845 'functionCall' => [Financial\CashFlow\Variable\NonPeriodic::class, 'presentValue'],
2846 'argumentCount' => '3',
2847 ],
2848 'XMATCH' => [
2849 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2850 'functionCall' => [Functions::class, 'DUMMY'],
2851 'argumentCount' => '2,3',
2852 ],
2853 'XOR' => [
2854 'category' => Category::CATEGORY_LOGICAL,
2855 'functionCall' => [Logical\Operations::class, 'logicalXor'],
2856 'argumentCount' => '1+',
2857 ],
2858 'YEAR' => [
2859 'category' => Category::CATEGORY_DATE_AND_TIME,
2860 'functionCall' => [DateTimeExcel\DateParts::class, 'year'],
2861 'argumentCount' => '1',
2862 ],
2863 'YEARFRAC' => [
2864 'category' => Category::CATEGORY_DATE_AND_TIME,
2865 'functionCall' => [DateTimeExcel\YearFrac::class, 'fraction'],
2866 'argumentCount' => '2,3',
2867 ],
2868 'YIELD' => [
2869 'category' => Category::CATEGORY_FINANCIAL,
2870 'functionCall' => [Functions::class, 'DUMMY'],
2871 'argumentCount' => '6,7',
2872 ],
2873 'YIELDDISC' => [
2874 'category' => Category::CATEGORY_FINANCIAL,
2875 'functionCall' => [Financial\Securities\Yields::class, 'yieldDiscounted'],
2876 'argumentCount' => '4,5',
2877 ],
2878 'YIELDMAT' => [
2879 'category' => Category::CATEGORY_FINANCIAL,
2880 'functionCall' => [Financial\Securities\Yields::class, 'yieldAtMaturity'],
2881 'argumentCount' => '5,6',
2882 ],
2883 'ZTEST' => [
2884 'category' => Category::CATEGORY_STATISTICAL,
2885 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'],
2886 'argumentCount' => '2-3',
2887 ],
2888 'Z.TEST' => [
2889 'category' => Category::CATEGORY_STATISTICAL,
2890 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'],
2891 'argumentCount' => '2-3',
2892 ],
2893 ];
2894
2895 /**
2896 * Internal functions used for special control purposes.
2897 *
2898 * @var array
2899 */
2900 private static $controlFunctions = [
2901 'MKMATRIX' => [
2902 'argumentCount' => '*',
2903 'functionCall' => [Internal\MakeMatrix::class, 'make'],
2904 ],
2905 'NAME.ERROR' => [
2906 'argumentCount' => '*',
2907 'functionCall' => [Functions::class, 'NAME'],
2908 ],
2909 'WILDCARDMATCH' => [
2910 'argumentCount' => '2',
2911 'functionCall' => [Internal\WildcardMatch::class, 'compare'],
2912 ],
2913 ];
2914
2915 public function __construct(?Spreadsheet $spreadsheet = null)
2916 {
2917 $this->spreadsheet = $spreadsheet;
2918 $this->cyclicReferenceStack = new CyclicReferenceStack();
2919 $this->debugLog = new Logger($this->cyclicReferenceStack);
2920 $this->branchPruner = new BranchPruner($this->branchPruningEnabled);
2921 }
2922
2923 private static function loadLocales(): void
2924 {
2925 $localeFileDirectory = __DIR__ . '/locale/';
2926 $localeFileNames = glob($localeFileDirectory . '*', GLOB_ONLYDIR) ?: [];
2927 foreach ($localeFileNames as $filename) {
2928 $filename = substr($filename, strlen($localeFileDirectory));
2929 if ($filename != 'en') {
2930 self::$validLocaleLanguages[] = $filename;
2931 }
2932 }
2933 }
2934
2935 /**
2936 * Get an instance of this class.
2937 *
2938 * @param ?Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object,
2939 * or NULL to create a standalone calculation engine
2940 */
2941 public static function getInstance(?Spreadsheet $spreadsheet = null): self
2942 {
2943 if ($spreadsheet !== null) {
2944 $instance = $spreadsheet->getCalculationEngine();
2945 if (isset($instance)) {
2946 return $instance;
2947 }
2948 }
2949
2950 if (!isset(self::$instance) || (self::$instance === null)) {
2951 self::$instance = new self();
2952 }
2953
2954 return self::$instance;
2955 }
2956
2957 /**
2958 * Flush the calculation cache for any existing instance of this class
2959 * but only if a Calculation instance exists.
2960 */
2961 public function flushInstance(): void
2962 {
2963 $this->clearCalculationCache();
2964 $this->branchPruner->clearBranchStore();
2965 }
2966
2967 /**
2968 * Get the Logger for this calculation engine instance.
2969 *
2970 * @return Logger
2971 */
2972 public function getDebugLog()
2973 {
2974 return $this->debugLog;
2975 }
2976
2977 /**
2978 * __clone implementation. Cloning should not be allowed in a Singleton!
2979 */
2980 final public function __clone()
2981 {
2982 throw new Exception('Cloning the calculation engine is not allowed!');
2983 }
2984
2985 /**
2986 * Return the locale-specific translation of TRUE.
2987 *
2988 * @return string locale-specific translation of TRUE
2989 */
2990 public static function getTRUE(): string
2991 {
2992 return self::$localeBoolean['TRUE'];
2993 }
2994
2995 /**
2996 * Return the locale-specific translation of FALSE.
2997 *
2998 * @return string locale-specific translation of FALSE
2999 */
3000 public static function getFALSE(): string
3001 {
3002 return self::$localeBoolean['FALSE'];
3003 }
3004
3005 /**
3006 * Set the Array Return Type (Array or Value of first element in the array).
3007 *
3008 * @param string $returnType Array return type
3009 *
3010 * @return bool Success or failure
3011 */
3012 public static function setArrayReturnType($returnType)
3013 {
3014 if (
3015 ($returnType == self::RETURN_ARRAY_AS_VALUE) ||
3016 ($returnType == self::RETURN_ARRAY_AS_ERROR) ||
3017 ($returnType == self::RETURN_ARRAY_AS_ARRAY)
3018 ) {
3019 self::$returnArrayAsType = $returnType;
3020
3021 return true;
3022 }
3023
3024 return false;
3025 }
3026
3027 /**
3028 * Return the Array Return Type (Array or Value of first element in the array).
3029 *
3030 * @return string $returnType Array return type
3031 */
3032 public static function getArrayReturnType()
3033 {
3034 return self::$returnArrayAsType;
3035 }
3036
3037 /**
3038 * Is calculation caching enabled?
3039 *
3040 * @return bool
3041 */
3042 public function getCalculationCacheEnabled()
3043 {
3044 return $this->calculationCacheEnabled;
3045 }
3046
3047 /**
3048 * Enable/disable calculation cache.
3049 *
3050 * @param bool $calculationCacheEnabled
3051 */
3052 public function setCalculationCacheEnabled($calculationCacheEnabled): void
3053 {
3054 $this->calculationCacheEnabled = $calculationCacheEnabled;
3055 $this->clearCalculationCache();
3056 }
3057
3058 /**
3059 * Enable calculation cache.
3060 */
3061 public function enableCalculationCache(): void
3062 {
3063 $this->setCalculationCacheEnabled(true);
3064 }
3065
3066 /**
3067 * Disable calculation cache.
3068 */
3069 public function disableCalculationCache(): void
3070 {
3071 $this->setCalculationCacheEnabled(false);
3072 }
3073
3074 /**
3075 * Clear calculation cache.
3076 */
3077 public function clearCalculationCache(): void
3078 {
3079 $this->calculationCache = [];
3080 }
3081
3082 /**
3083 * Clear calculation cache for a specified worksheet.
3084 *
3085 * @param string $worksheetName
3086 */
3087 public function clearCalculationCacheForWorksheet($worksheetName): void
3088 {
3089 if (isset($this->calculationCache[$worksheetName])) {
3090 unset($this->calculationCache[$worksheetName]);
3091 }
3092 }
3093
3094 /**
3095 * Rename calculation cache for a specified worksheet.
3096 *
3097 * @param string $fromWorksheetName
3098 * @param string $toWorksheetName
3099 */
3100 public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName): void
3101 {
3102 if (isset($this->calculationCache[$fromWorksheetName])) {
3103 $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName];
3104 unset($this->calculationCache[$fromWorksheetName]);
3105 }
3106 }
3107
3108 /**
3109 * Enable/disable calculation cache.
3110 *
3111 * @param mixed $enabled
3112 */
3113 public function setBranchPruningEnabled($enabled): void
3114 {
3115 $this->branchPruningEnabled = $enabled;
3116 $this->branchPruner = new BranchPruner($this->branchPruningEnabled);
3117 }
3118
3119 public function enableBranchPruning(): void
3120 {
3121 $this->setBranchPruningEnabled(true);
3122 }
3123
3124 public function disableBranchPruning(): void
3125 {
3126 $this->setBranchPruningEnabled(false);
3127 }
3128
3129 /**
3130 * Get the currently defined locale code.
3131 *
3132 * @return string
3133 */
3134 public function getLocale()
3135 {
3136 return self::$localeLanguage;
3137 }
3138
3139 private function getLocaleFile(string $localeDir, string $locale, string $language, string $file): string
3140 {
3141 $localeFileName = $localeDir . str_replace('_', DIRECTORY_SEPARATOR, $locale) .
3142 DIRECTORY_SEPARATOR . $file;
3143 if (!file_exists($localeFileName)) {
3144 // If there isn't a locale specific file, look for a language specific file
3145 $localeFileName = $localeDir . $language . DIRECTORY_SEPARATOR . $file;
3146 if (!file_exists($localeFileName)) {
3147 throw new Exception('Locale file not found');
3148 }
3149 }
3150
3151 return $localeFileName;
3152 }
3153
3154 /**
3155 * Set the locale code.
3156 *
3157 * @param string $locale The locale to use for formula translation, eg: 'en_us'
3158 *
3159 * @return bool
3160 */
3161 public function setLocale(string $locale)
3162 {
3163 // Identify our locale and language
3164 $language = $locale = strtolower($locale);
3165 if (strpos($locale, '_') !== false) {
3166 [$language] = explode('_', $locale);
3167 }
3168 if (count(self::$validLocaleLanguages) == 1) {
3169 self::loadLocales();
3170 }
3171
3172 // Test whether we have any language data for this language (any locale)
3173 if (in_array($language, self::$validLocaleLanguages, true)) {
3174 // initialise language/locale settings
3175 self::$localeFunctions = [];
3176 self::$localeArgumentSeparator = ',';
3177 self::$localeBoolean = ['TRUE' => 'TRUE', 'FALSE' => 'FALSE', 'NULL' => 'NULL'];
3178
3179 // Default is US English, if user isn't requesting US english, then read the necessary data from the locale files
3180 if ($locale !== 'en_us') {
3181 $localeDir = implode(DIRECTORY_SEPARATOR, [__DIR__, 'locale', null]);
3182 // Search for a file with a list of function names for locale
3183 try {
3184 $functionNamesFile = $this->getLocaleFile($localeDir, $locale, $language, 'functions');
3185 } catch (Exception $e) {
3186 return false;
3187 }
3188
3189 // Retrieve the list of locale or language specific function names
3190 $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
3191 foreach ($localeFunctions as $localeFunction) {
3192 [$localeFunction] = explode('##', $localeFunction); // Strip out comments
3193 if (strpos($localeFunction, '=') !== false) {
3194 [$fName, $lfName] = array_map('trim', explode('=', $localeFunction));
3195 if ((substr($fName, 0, 1) === '*' || isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) {
3196 self::$localeFunctions[$fName] = $lfName;
3197 }
3198 }
3199 }
3200 // Default the TRUE and FALSE constants to the locale names of the TRUE() and FALSE() functions
3201 if (isset(self::$localeFunctions['TRUE'])) {
3202 self::$localeBoolean['TRUE'] = self::$localeFunctions['TRUE'];
3203 }
3204 if (isset(self::$localeFunctions['FALSE'])) {
3205 self::$localeBoolean['FALSE'] = self::$localeFunctions['FALSE'];
3206 }
3207
3208 try {
3209 $configFile = $this->getLocaleFile($localeDir, $locale, $language, 'config');
3210 } catch (Exception $e) {
3211 return false;
3212 }
3213
3214 $localeSettings = file($configFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
3215 foreach ($localeSettings as $localeSetting) {
3216 [$localeSetting] = explode('##', $localeSetting); // Strip out comments
3217 if (strpos($localeSetting, '=') !== false) {
3218 [$settingName, $settingValue] = array_map('trim', explode('=', $localeSetting));
3219 $settingName = strtoupper($settingName);
3220 if ($settingValue !== '') {
3221 switch ($settingName) {
3222 case 'ARGUMENTSEPARATOR':
3223 self::$localeArgumentSeparator = $settingValue;
3224
3225 break;
3226 }
3227 }
3228 }
3229 }
3230 }
3231
3232 self::$functionReplaceFromExcel = self::$functionReplaceToExcel =
3233 self::$functionReplaceFromLocale = self::$functionReplaceToLocale = null;
3234 self::$localeLanguage = $locale;
3235
3236 return true;
3237 }
3238
3239 return false;
3240 }
3241
3242 public static function translateSeparator(
3243 string $fromSeparator,
3244 string $toSeparator,
3245 string $formula,
3246 int &$inBracesLevel,
3247 string $openBrace = self::FORMULA_OPEN_FUNCTION_BRACE,
3248 string $closeBrace = self::FORMULA_CLOSE_FUNCTION_BRACE
3249 ): string {
3250 $strlen = mb_strlen($formula);
3251 for ($i = 0; $i < $strlen; ++$i) {
3252 $chr = mb_substr($formula, $i, 1);
3253 switch ($chr) {
3254 case $openBrace:
3255 ++$inBracesLevel;
3256
3257 break;
3258 case $closeBrace:
3259 --$inBracesLevel;
3260
3261 break;
3262 case $fromSeparator:
3263 if ($inBracesLevel > 0) {
3264 $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1);
3265 }
3266 }
3267 }
3268
3269 return $formula;
3270 }
3271
3272 private static function translateFormulaBlock(
3273 array $from,
3274 array $to,
3275 string $formula,
3276 int &$inFunctionBracesLevel,
3277 int &$inMatrixBracesLevel,
3278 string $fromSeparator,
3279 string $toSeparator
3280 ): string {
3281 // Function Names
3282 $formula = (string) preg_replace($from, $to, $formula);
3283
3284 // Temporarily adjust matrix separators so that they won't be confused with function arguments
3285 $formula = self::translateSeparator(';', '|', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
3286 $formula = self::translateSeparator(',', '!', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
3287 // Function Argument Separators
3288 $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inFunctionBracesLevel);
3289 // Restore matrix separators
3290 $formula = self::translateSeparator('|', ';', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
3291 $formula = self::translateSeparator('!', ',', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE);
3292
3293 return $formula;
3294 }
3295
3296 private static function translateFormula(array $from, array $to, string $formula, string $fromSeparator, string $toSeparator): string
3297 {
3298 // Convert any Excel function names and constant names to the required language;
3299 // and adjust function argument separators
3300 if (self::$localeLanguage !== 'en_us') {
3301 $inFunctionBracesLevel = 0;
3302 $inMatrixBracesLevel = 0;
3303 // If there is the possibility of separators within a quoted string, then we treat them as literals
3304 if (strpos($formula, self::FORMULA_STRING_QUOTE) !== false) {
3305 // So instead we skip replacing in any quoted strings by only replacing in every other array element
3306 // after we've exploded the formula
3307 $temp = explode(self::FORMULA_STRING_QUOTE, $formula);
3308 $notWithinQuotes = false;
3309 foreach ($temp as &$value) {
3310 // Only adjust in alternating array entries
3311 $notWithinQuotes = $notWithinQuotes === false;
3312 if ($notWithinQuotes === true) {
3313 $value = self::translateFormulaBlock($from, $to, $value, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator);
3314 }
3315 }
3316 unset($value);
3317 // Then rebuild the formula string
3318 $formula = implode(self::FORMULA_STRING_QUOTE, $temp);
3319 } else {
3320 // If there's no quoted strings, then we do a simple count/replace
3321 $formula = self::translateFormulaBlock($from, $to, $formula, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator);
3322 }
3323 }
3324
3325 return $formula;
3326 }
3327
3328 /** @var ?array */
3329 private static $functionReplaceFromExcel;
3330
3331 /** @var ?array */
3332 private static $functionReplaceToLocale;
3333
3334 /**
3335 * @param string $formula
3336 *
3337 * @return string
3338 */
3339 public function _translateFormulaToLocale($formula)
3340 {
3341 // Build list of function names and constants for translation
3342 if (self::$functionReplaceFromExcel === null) {
3343 self::$functionReplaceFromExcel = [];
3344 foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
3345 self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/ui';
3346 }
3347 foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
3348 self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui';
3349 }
3350 }
3351
3352 if (self::$functionReplaceToLocale === null) {
3353 self::$functionReplaceToLocale = [];
3354 foreach (self::$localeFunctions as $localeFunctionName) {
3355 self::$functionReplaceToLocale[] = '$1' . trim($localeFunctionName) . '$2';
3356 }
3357 foreach (self::$localeBoolean as $localeBoolean) {
3358 self::$functionReplaceToLocale[] = '$1' . trim($localeBoolean) . '$2';
3359 }
3360 }
3361
3362 return self::translateFormula(
3363 self::$functionReplaceFromExcel,
3364 self::$functionReplaceToLocale,
3365 $formula,
3366 ',',
3367 self::$localeArgumentSeparator
3368 );
3369 }
3370
3371 /** @var ?array */
3372 private static $functionReplaceFromLocale;
3373
3374 /** @var ?array */
3375 private static $functionReplaceToExcel;
3376
3377 /**
3378 * @param string $formula
3379 *
3380 * @return string
3381 */
3382 public function _translateFormulaToEnglish($formula)
3383 {
3384 if (self::$functionReplaceFromLocale === null) {
3385 self::$functionReplaceFromLocale = [];
3386 foreach (self::$localeFunctions as $localeFunctionName) {
3387 self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/ui';
3388 }
3389 foreach (self::$localeBoolean as $excelBoolean) {
3390 self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui';
3391 }
3392 }
3393
3394 if (self::$functionReplaceToExcel === null) {
3395 self::$functionReplaceToExcel = [];
3396 foreach (array_keys(self::$localeFunctions) as $excelFunctionName) {
3397 self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2';
3398 }
3399 foreach (array_keys(self::$localeBoolean) as $excelBoolean) {
3400 self::$functionReplaceToExcel[] = '$1' . trim($excelBoolean) . '$2';
3401 }
3402 }
3403
3404 return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ',');
3405 }
3406
3407 /**
3408 * @param string $function
3409 *
3410 * @return string
3411 */
3412 public static function localeFunc($function)
3413 {
3414 if (self::$localeLanguage !== 'en_us') {
3415 $functionName = trim($function, '(');
3416 if (isset(self::$localeFunctions[$functionName])) {
3417 $brace = ($functionName != $function);
3418 $function = self::$localeFunctions[$functionName];
3419 if ($brace) {
3420 $function .= '(';
3421 }
3422 }
3423 }
3424
3425 return $function;
3426 }
3427
3428 /**
3429 * Wrap string values in quotes.
3430 *
3431 * @param mixed $value
3432 *
3433 * @return mixed
3434 */
3435 public static function wrapResult($value)
3436 {
3437 if (is_string($value)) {
3438 // Error values cannot be "wrapped"
3439 if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
3440 // Return Excel errors "as is"
3441 return $value;
3442 }
3443
3444 // Return strings wrapped in quotes
3445 return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
3446 } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
3447 // Convert numeric errors to NaN error
3448 return Information\ExcelError::NAN();
3449 }
3450
3451 return $value;
3452 }
3453
3454 /**
3455 * Remove quotes used as a wrapper to identify string values.
3456 *
3457 * @param mixed $value
3458 *
3459 * @return mixed
3460 */
3461 public static function unwrapResult($value)
3462 {
3463 if (is_string($value)) {
3464 if ((isset($value[0])) && ($value[0] == self::FORMULA_STRING_QUOTE) && (substr($value, -1) == self::FORMULA_STRING_QUOTE)) {
3465 return substr($value, 1, -1);
3466 }
3467 // Convert numeric errors to NAN error
3468 } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) {
3469 return Information\ExcelError::NAN();
3470 }
3471
3472 return $value;
3473 }
3474
3475 /**
3476 * Calculate cell value (using formula from a cell ID)
3477 * Retained for backward compatibility.
3478 *
3479 * @param Cell $cell Cell to calculate
3480 *
3481 * @return mixed
3482 */
3483 public function calculate(?Cell $cell = null)
3484 {
3485 try {
3486 return $this->calculateCellValue($cell);
3487 } catch (\Exception $e) {
3488 throw new Exception($e->getMessage());
3489 }
3490 }
3491
3492 /**
3493 * Calculate the value of a cell formula.
3494 *
3495 * @param Cell $cell Cell to calculate
3496 * @param bool $resetLog Flag indicating whether the debug log should be reset or not
3497 *
3498 * @return mixed
3499 */
3500 public function calculateCellValue(?Cell $cell = null, $resetLog = true)
3501 {
3502 if ($cell === null) {
3503 return null;
3504 }
3505
3506 $returnArrayAsType = self::$returnArrayAsType;
3507 if ($resetLog) {
3508 // Initialise the logging settings if requested
3509 $this->formulaError = null;
3510 $this->debugLog->clearLog();
3511 $this->cyclicReferenceStack->clear();
3512 $this->cyclicFormulaCounter = 1;
3513
3514 self::$returnArrayAsType = self::RETURN_ARRAY_AS_ARRAY;
3515 }
3516
3517 // Execute the calculation for the cell formula
3518 $this->cellStack[] = [
3519 'sheet' => $cell->getWorksheet()->getTitle(),
3520 'cell' => $cell->getCoordinate(),
3521 ];
3522
3523 $cellAddressAttempted = false;
3524 $cellAddress = null;
3525
3526 try {
3527 $result = self::unwrapResult($this->_calculateFormulaValue($cell->getValue(), $cell->getCoordinate(), $cell));
3528 if ($this->spreadsheet === null) {
3529 throw new Exception('null spreadsheet in calculateCellValue');
3530 }
3531 $cellAddressAttempted = true;
3532 $cellAddress = array_pop($this->cellStack);
3533 if ($cellAddress === null) {
3534 throw new Exception('null cellAddress in calculateCellValue');
3535 }
3536 $testSheet = $this->spreadsheet->getSheetByName($cellAddress['sheet']);
3537 if ($testSheet === null) {
3538 throw new Exception('worksheet not found in calculateCellValue');
3539 }
3540 $testSheet->getCell($cellAddress['cell']);
3541 } catch (\Exception $e) {
3542 if (!$cellAddressAttempted) {
3543 $cellAddress = array_pop($this->cellStack);
3544 }
3545 if ($this->spreadsheet !== null && is_array($cellAddress) && array_key_exists('sheet', $cellAddress)) {
3546 $testSheet = $this->spreadsheet->getSheetByName($cellAddress['sheet']);
3547 if ($testSheet !== null && array_key_exists('cell', $cellAddress)) {
3548 $testSheet->getCell($cellAddress['cell']);
3549 }
3550 }
3551
3552 throw new Exception($e->getMessage(), $e->getCode(), $e);
3553 }
3554
3555 if ((is_array($result)) && (self::$returnArrayAsType != self::RETURN_ARRAY_AS_ARRAY)) {
3556 self::$returnArrayAsType = $returnArrayAsType;
3557 $testResult = Functions::flattenArray($result);
3558 if (self::$returnArrayAsType == self::RETURN_ARRAY_AS_ERROR) {
3559 return Information\ExcelError::VALUE();
3560 }
3561 // If there's only a single cell in the array, then we allow it
3562 if (count($testResult) != 1) {
3563 // If keys are numeric, then it's a matrix result rather than a cell range result, so we permit it
3564 $r = array_keys($result);
3565 $r = array_shift($r);
3566 if (!is_numeric($r)) {
3567 return Information\ExcelError::VALUE();
3568 }
3569 if (is_array($result[$r])) {
3570 $c = array_keys($result[$r]);
3571 $c = array_shift($c);
3572 if (!is_numeric($c)) {
3573 return Information\ExcelError::VALUE();
3574 }
3575 }
3576 }
3577 $result = array_shift($testResult);
3578 }
3579 self::$returnArrayAsType = $returnArrayAsType;
3580
3581 if ($result === null && $cell->getWorksheet()->getSheetView()->getShowZeros()) {
3582 return 0;
3583 } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) {
3584 return Information\ExcelError::NAN();
3585 }
3586
3587 return $result;
3588 }
3589
3590 /**
3591 * Validate and parse a formula string.
3592 *
3593 * @param string $formula Formula to parse
3594 *
3595 * @return array|bool
3596 */
3597 public function parseFormula($formula)
3598 {
3599 // Basic validation that this is indeed a formula
3600 // We return an empty array if not
3601 $formula = trim($formula);
3602 if ((!isset($formula[0])) || ($formula[0] != '=')) {
3603 return [];
3604 }
3605 $formula = ltrim(substr($formula, 1));
3606 if (!isset($formula[0])) {
3607 return [];
3608 }
3609
3610 // Parse the formula and return the token stack
3611 return $this->internalParseFormula($formula);
3612 }
3613
3614 /**
3615 * Calculate the value of a formula.
3616 *
3617 * @param string $formula Formula to parse
3618 * @param string $cellID Address of the cell to calculate
3619 * @param Cell $cell Cell to calculate
3620 *
3621 * @return mixed
3622 */
3623 public function calculateFormula($formula, $cellID = null, ?Cell $cell = null)
3624 {
3625 // Initialise the logging settings
3626 $this->formulaError = null;
3627 $this->debugLog->clearLog();
3628 $this->cyclicReferenceStack->clear();
3629
3630 $resetCache = $this->getCalculationCacheEnabled();
3631 if ($this->spreadsheet !== null && $cellID === null && $cell === null) {
3632 $cellID = 'A1';
3633 $cell = $this->spreadsheet->getActiveSheet()->getCell($cellID);
3634 } else {
3635 // Disable calculation cacheing because it only applies to cell calculations, not straight formulae
3636 // But don't actually flush any cache
3637 $this->calculationCacheEnabled = false;
3638 }
3639
3640 // Execute the calculation
3641 try {
3642 $result = self::unwrapResult($this->_calculateFormulaValue($formula, $cellID, $cell));
3643 } catch (\Exception $e) {
3644 throw new Exception($e->getMessage());
3645 }
3646
3647 if ($this->spreadsheet === null) {
3648 // Reset calculation cacheing to its previous state
3649 $this->calculationCacheEnabled = $resetCache;
3650 }
3651
3652 return $result;
3653 }
3654
3655 /**
3656 * @param mixed $cellValue
3657 */
3658 public function getValueFromCache(string $cellReference, &$cellValue): bool
3659 {
3660 $this->debugLog->writeDebugLog('Testing cache value for cell %s', $cellReference);
3661 // Is calculation cacheing enabled?
3662 // If so, is the required value present in calculation cache?
3663 if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
3664 $this->debugLog->writeDebugLog('Retrieving value for cell %s from cache', $cellReference);
3665 // Return the cached result
3666
3667 $cellValue = $this->calculationCache[$cellReference];
3668
3669 return true;
3670 }
3671
3672 return false;
3673 }
3674
3675 /**
3676 * @param string $cellReference
3677 * @param mixed $cellValue
3678 */
3679 public function saveValueToCache($cellReference, $cellValue): void
3680 {
3681 if ($this->calculationCacheEnabled) {
3682 $this->calculationCache[$cellReference] = $cellValue;
3683 }
3684 }
3685
3686 /**
3687 * Parse a cell formula and calculate its value.
3688 *
3689 * @param string $formula The formula to parse and calculate
3690 * @param string $cellID The ID (e.g. A3) of the cell that we are calculating
3691 * @param Cell $cell Cell to calculate
3692 * @param bool $ignoreQuotePrefix If set to true, evaluate the formyla even if the referenced cell is quote prefixed
3693 *
3694 * @return mixed
3695 */
3696 public function _calculateFormulaValue($formula, $cellID = null, ?Cell $cell = null, bool $ignoreQuotePrefix = false)
3697 {
3698 $cellValue = null;
3699
3700 // Quote-Prefixed cell values cannot be formulae, but are treated as strings
3701 if ($cell !== null && $ignoreQuotePrefix === false && $cell->getStyle()->getQuotePrefix() === true) {
3702 return self::wrapResult((string) $formula);
3703 }
3704
3705 if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) {
3706 return self::wrapResult($formula);
3707 }
3708
3709 // Basic validation that this is indeed a formula
3710 // We simply return the cell value if not
3711 $formula = trim($formula);
3712 if ($formula[0] != '=') {
3713 return self::wrapResult($formula);
3714 }
3715 $formula = ltrim(substr($formula, 1));
3716 if (!isset($formula[0])) {
3717 return self::wrapResult($formula);
3718 }
3719
3720 $pCellParent = ($cell !== null) ? $cell->getWorksheet() : null;
3721 $wsTitle = ($pCellParent !== null) ? $pCellParent->getTitle() : "\x00Wrk";
3722 $wsCellReference = $wsTitle . '!' . $cellID;
3723
3724 if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) {
3725 return $cellValue;
3726 }
3727 $this->debugLog->writeDebugLog('Evaluating formula for cell %s', $wsCellReference);
3728
3729 if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) {
3730 if ($this->cyclicFormulaCount <= 0) {
3731 $this->cyclicFormulaCell = '';
3732
3733 return $this->raiseFormulaError('Cyclic Reference in Formula');
3734 } elseif ($this->cyclicFormulaCell === $wsCellReference) {
3735 ++$this->cyclicFormulaCounter;
3736 if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
3737 $this->cyclicFormulaCell = '';
3738
3739 return $cellValue;
3740 }
3741 } elseif ($this->cyclicFormulaCell == '') {
3742 if ($this->cyclicFormulaCounter >= $this->cyclicFormulaCount) {
3743 return $cellValue;
3744 }
3745 $this->cyclicFormulaCell = $wsCellReference;
3746 }
3747 }
3748
3749 $this->debugLog->writeDebugLog('Formula for cell %s is %s', $wsCellReference, $formula);
3750 // Parse the formula onto the token stack and calculate the value
3751 $this->cyclicReferenceStack->push($wsCellReference);
3752
3753 $cellValue = $this->processTokenStack($this->internalParseFormula($formula, $cell), $cellID, $cell);
3754 $this->cyclicReferenceStack->pop();
3755
3756 // Save to calculation cache
3757 if ($cellID !== null) {
3758 $this->saveValueToCache($wsCellReference, $cellValue);
3759 }
3760
3761 // Return the calculated value
3762 return $cellValue;
3763 }
3764
3765 /**
3766 * Ensure that paired matrix operands are both matrices and of the same size.
3767 *
3768 * @param mixed $operand1 First matrix operand
3769 * @param mixed $operand2 Second matrix operand
3770 * @param int $resize Flag indicating whether the matrices should be resized to match
3771 * and (if so), whether the smaller dimension should grow or the
3772 * larger should shrink.
3773 * 0 = no resize
3774 * 1 = shrink to fit
3775 * 2 = extend to fit
3776 *
3777 * @return array
3778 */
3779 private static function checkMatrixOperands(&$operand1, &$operand2, $resize = 1)
3780 {
3781 // Examine each of the two operands, and turn them into an array if they aren't one already
3782 // Note that this function should only be called if one or both of the operand is already an array
3783 if (!is_array($operand1)) {
3784 [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand2);
3785 $operand1 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand1));
3786 $resize = 0;
3787 } elseif (!is_array($operand2)) {
3788 [$matrixRows, $matrixColumns] = self::getMatrixDimensions($operand1);
3789 $operand2 = array_fill(0, $matrixRows, array_fill(0, $matrixColumns, $operand2));
3790 $resize = 0;
3791 }
3792
3793 [$matrix1Rows, $matrix1Columns] = self::getMatrixDimensions($operand1);
3794 [$matrix2Rows, $matrix2Columns] = self::getMatrixDimensions($operand2);
3795 if (($matrix1Rows == $matrix2Columns) && ($matrix2Rows == $matrix1Columns)) {
3796 $resize = 1;
3797 }
3798
3799 if ($resize == 2) {
3800 // Given two matrices of (potentially) unequal size, convert the smaller in each dimension to match the larger
3801 self::resizeMatricesExtend($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
3802 } elseif ($resize == 1) {
3803 // Given two matrices of (potentially) unequal size, convert the larger in each dimension to match the smaller
3804 self::resizeMatricesShrink($operand1, $operand2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns);
3805 }
3806
3807 return [$matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns];
3808 }
3809
3810 /**
3811 * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0.
3812 *
3813 * @param array $matrix matrix operand
3814 *
3815 * @return int[] An array comprising the number of rows, and number of columns
3816 */
3817 public static function getMatrixDimensions(array &$matrix)
3818 {
3819 $matrixRows = count($matrix);
3820 $matrixColumns = 0;
3821 foreach ($matrix as $rowKey => $rowValue) {
3822 if (!is_array($rowValue)) {
3823 $matrix[$rowKey] = [$rowValue];
3824 $matrixColumns = max(1, $matrixColumns);
3825 } else {
3826 $matrix[$rowKey] = array_values($rowValue);
3827 $matrixColumns = max(count($rowValue), $matrixColumns);
3828 }
3829 }
3830 $matrix = array_values($matrix);
3831
3832 return [$matrixRows, $matrixColumns];
3833 }
3834
3835 /**
3836 * Ensure that paired matrix operands are both matrices of the same size.
3837 *
3838 * @param mixed $matrix1 First matrix operand
3839 * @param mixed $matrix2 Second matrix operand
3840 * @param int $matrix1Rows Row size of first matrix operand
3841 * @param int $matrix1Columns Column size of first matrix operand
3842 * @param int $matrix2Rows Row size of second matrix operand
3843 * @param int $matrix2Columns Column size of second matrix operand
3844 */
3845 private static function resizeMatricesShrink(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns): void
3846 {
3847 if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3848 if ($matrix2Rows < $matrix1Rows) {
3849 for ($i = $matrix2Rows; $i < $matrix1Rows; ++$i) {
3850 unset($matrix1[$i]);
3851 }
3852 }
3853 if ($matrix2Columns < $matrix1Columns) {
3854 for ($i = 0; $i < $matrix1Rows; ++$i) {
3855 for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3856 unset($matrix1[$i][$j]);
3857 }
3858 }
3859 }
3860 }
3861
3862 if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3863 if ($matrix1Rows < $matrix2Rows) {
3864 for ($i = $matrix1Rows; $i < $matrix2Rows; ++$i) {
3865 unset($matrix2[$i]);
3866 }
3867 }
3868 if ($matrix1Columns < $matrix2Columns) {
3869 for ($i = 0; $i < $matrix2Rows; ++$i) {
3870 for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3871 unset($matrix2[$i][$j]);
3872 }
3873 }
3874 }
3875 }
3876 }
3877
3878 /**
3879 * Ensure that paired matrix operands are both matrices of the same size.
3880 *
3881 * @param mixed $matrix1 First matrix operand
3882 * @param mixed $matrix2 Second matrix operand
3883 * @param int $matrix1Rows Row size of first matrix operand
3884 * @param int $matrix1Columns Column size of first matrix operand
3885 * @param int $matrix2Rows Row size of second matrix operand
3886 * @param int $matrix2Columns Column size of second matrix operand
3887 */
3888 private static function resizeMatricesExtend(&$matrix1, &$matrix2, $matrix1Rows, $matrix1Columns, $matrix2Rows, $matrix2Columns): void
3889 {
3890 if (($matrix2Columns < $matrix1Columns) || ($matrix2Rows < $matrix1Rows)) {
3891 if ($matrix2Columns < $matrix1Columns) {
3892 for ($i = 0; $i < $matrix2Rows; ++$i) {
3893 $x = $matrix2[$i][$matrix2Columns - 1];
3894 for ($j = $matrix2Columns; $j < $matrix1Columns; ++$j) {
3895 $matrix2[$i][$j] = $x;
3896 }
3897 }
3898 }
3899 if ($matrix2Rows < $matrix1Rows) {
3900 $x = $matrix2[$matrix2Rows - 1];
3901 for ($i = 0; $i < $matrix1Rows; ++$i) {
3902 $matrix2[$i] = $x;
3903 }
3904 }
3905 }
3906
3907 if (($matrix1Columns < $matrix2Columns) || ($matrix1Rows < $matrix2Rows)) {
3908 if ($matrix1Columns < $matrix2Columns) {
3909 for ($i = 0; $i < $matrix1Rows; ++$i) {
3910 $x = $matrix1[$i][$matrix1Columns - 1];
3911 for ($j = $matrix1Columns; $j < $matrix2Columns; ++$j) {
3912 $matrix1[$i][$j] = $x;
3913 }
3914 }
3915 }
3916 if ($matrix1Rows < $matrix2Rows) {
3917 $x = $matrix1[$matrix1Rows - 1];
3918 for ($i = 0; $i < $matrix2Rows; ++$i) {
3919 $matrix1[$i] = $x;
3920 }
3921 }
3922 }
3923 }
3924
3925 /**
3926 * Format details of an operand for display in the log (based on operand type).
3927 *
3928 * @param mixed $value First matrix operand
3929 *
3930 * @return mixed
3931 */
3932 private function showValue($value)
3933 {
3934 if ($this->debugLog->getWriteDebugLog()) {
3935 $testArray = Functions::flattenArray($value);
3936 if (count($testArray) == 1) {
3937 $value = array_pop($testArray);
3938 }
3939
3940 if (is_array($value)) {
3941 $returnMatrix = [];
3942 $pad = $rpad = ', ';
3943 foreach ($value as $row) {
3944 if (is_array($row)) {
3945 $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
3946 $rpad = '; ';
3947 } else {
3948 $returnMatrix[] = $this->showValue($row);
3949 }
3950 }
3951
3952 return '{ ' . implode($rpad, $returnMatrix) . ' }';
3953 } elseif (is_string($value) && (trim($value, self::FORMULA_STRING_QUOTE) == $value)) {
3954 return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
3955 } elseif (is_bool($value)) {
3956 return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
3957 } elseif ($value === null) {
3958 return self::$localeBoolean['NULL'];
3959 }
3960 }
3961
3962 return Functions::flattenSingleValue($value);
3963 }
3964
3965 /**
3966 * Format type and details of an operand for display in the log (based on operand type).
3967 *
3968 * @param mixed $value First matrix operand
3969 *
3970 * @return null|string
3971 */
3972 private function showTypeDetails($value)
3973 {
3974 if ($this->debugLog->getWriteDebugLog()) {
3975 $testArray = Functions::flattenArray($value);
3976 if (count($testArray) == 1) {
3977 $value = array_pop($testArray);
3978 }
3979
3980 if ($value === null) {
3981 return 'a NULL value';
3982 } elseif (is_float($value)) {
3983 $typeString = 'a floating point number';
3984 } elseif (is_int($value)) {
3985 $typeString = 'an integer number';
3986 } elseif (is_bool($value)) {
3987 $typeString = 'a boolean';
3988 } elseif (is_array($value)) {
3989 $typeString = 'a matrix';
3990 } else {
3991 if ($value == '') {
3992 return 'an empty string';
3993 } elseif ($value[0] == '#') {
3994 return 'a ' . $value . ' error';
3995 }
3996 $typeString = 'a string';
3997 }
3998
3999 return $typeString . ' with a value of ' . $this->showValue($value);
4000 }
4001
4002 return null;
4003 }
4004
4005 /**
4006 * @param string $formula
4007 *
4008 * @return false|string False indicates an error
4009 */
4010 private function convertMatrixReferences($formula)
4011 {
4012 static $matrixReplaceFrom = [self::FORMULA_OPEN_MATRIX_BRACE, ';', self::FORMULA_CLOSE_MATRIX_BRACE];
4013 static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))'];
4014
4015 // Convert any Excel matrix references to the MKMATRIX() function
4016 if (strpos($formula, self::FORMULA_OPEN_MATRIX_BRACE) !== false) {
4017 // If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators
4018 if (strpos($formula, self::FORMULA_STRING_QUOTE) !== false) {
4019 // So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded
4020 // the formula
4021 $temp = explode(self::FORMULA_STRING_QUOTE, $formula);
4022 // Open and Closed counts used for trapping mismatched braces in the formula
4023 $openCount = $closeCount = 0;
4024 $notWithinQuotes = false;
4025 foreach ($temp as &$value) {
4026 // Only count/replace in alternating array entries
4027 $notWithinQuotes = $notWithinQuotes === false;
4028 if ($notWithinQuotes === true) {
4029 $openCount += substr_count($value, self::FORMULA_OPEN_MATRIX_BRACE);
4030 $closeCount += substr_count($value, self::FORMULA_CLOSE_MATRIX_BRACE);
4031 $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value);
4032 }
4033 }
4034 unset($value);
4035 // Then rebuild the formula string
4036 $formula = implode(self::FORMULA_STRING_QUOTE, $temp);
4037 } else {
4038 // If there's no quoted strings, then we do a simple count/replace
4039 $openCount = substr_count($formula, self::FORMULA_OPEN_MATRIX_BRACE);
4040 $closeCount = substr_count($formula, self::FORMULA_CLOSE_MATRIX_BRACE);
4041 $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula);
4042 }
4043 // Trap for mismatched braces and trigger an appropriate error
4044 if ($openCount < $closeCount) {
4045 if ($openCount > 0) {
4046 return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '}'");
4047 }
4048
4049 return $this->raiseFormulaError("Formula Error: Unexpected '}' encountered");
4050 } elseif ($openCount > $closeCount) {
4051 if ($closeCount > 0) {
4052 return $this->raiseFormulaError("Formula Error: Mismatched matrix braces '{'");
4053 }
4054
4055 return $this->raiseFormulaError("Formula Error: Unexpected '{' encountered");
4056 }
4057 }
4058
4059 return $formula;
4060 }
4061
4062 /**
4063 * Binary Operators.
4064 * These operators always work on two values.
4065 * Array key is the operator, the value indicates whether this is a left or right associative operator.
4066 *
4067 * @var array
4068 */
4069 private static $operatorAssociativity = [
4070 '^' => 0, // Exponentiation
4071 '*' => 0, '/' => 0, // Multiplication and Division
4072 '+' => 0, '-' => 0, // Addition and Subtraction
4073 '&' => 0, // Concatenation
4074 '' => 0, '' => 0, ':' => 0, // Union, Intersect and Range
4075 '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison
4076 ];
4077
4078 /**
4079 * Comparison (Boolean) Operators.
4080 * These operators work on two values, but always return a boolean result.
4081 *
4082 * @var array
4083 */
4084 private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true];
4085
4086 /**
4087 * Operator Precedence.
4088 * This list includes all valid operators, whether binary (including boolean) or unary (such as %).
4089 * Array key is the operator, the value is its precedence.
4090 *
4091 * @var array
4092 */
4093 private static $operatorPrecedence = [
4094 ':' => 9, // Range
4095 '' => 8, // Intersect
4096 '' => 7, // Union
4097 '~' => 6, // Negation
4098 '%' => 5, // Percentage
4099 '^' => 4, // Exponentiation
4100 '*' => 3, '/' => 3, // Multiplication and Division
4101 '+' => 2, '-' => 2, // Addition and Subtraction
4102 '&' => 1, // Concatenation
4103 '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison
4104 ];
4105
4106 // Convert infix to postfix notation
4107
4108 /**
4109 * @param string $formula
4110 *
4111 * @return array<int, mixed>|false
4112 */
4113 private function internalParseFormula($formula, ?Cell $cell = null)
4114 {
4115 if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
4116 return false;
4117 }
4118
4119 // If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
4120 // so we store the parent worksheet so that we can re-attach it when necessary
4121 $pCellParent = ($cell !== null) ? $cell->getWorksheet() : null;
4122
4123 $regexpMatchString = '/^((?<string>' . self::CALCULATION_REGEXP_STRING .
4124 ')|(?<function>' . self::CALCULATION_REGEXP_FUNCTION .
4125 ')|(?<cellRef>' . self::CALCULATION_REGEXP_CELLREF .
4126 ')|(?<colRange>' . self::CALCULATION_REGEXP_COLUMN_RANGE .
4127 ')|(?<rowRange>' . self::CALCULATION_REGEXP_ROW_RANGE .
4128 ')|(?<number>' . self::CALCULATION_REGEXP_NUMBER .
4129 ')|(?<openBrace>' . self::CALCULATION_REGEXP_OPENBRACE .
4130 ')|(?<structuredReference>' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE .
4131 ')|(?<definedName>' . self::CALCULATION_REGEXP_DEFINEDNAME .
4132 ')|(?<error>' . self::CALCULATION_REGEXP_ERROR .
4133 '))/sui';
4134
4135 // Start with initialisation
4136 $index = 0;
4137 $stack = new Stack($this->branchPruner);
4138 $output = [];
4139 $expectingOperator = false; // We use this test in syntax-checking the expression to determine when a
4140 // - is a negation or + is a positive operator rather than an operation
4141 $expectingOperand = false; // We use this test in syntax-checking the expression to determine whether an operand
4142 // should be null in a function call
4143
4144 // The guts of the lexical parser
4145 // Loop through the formula extracting each operator and operand in turn
4146 while (true) {
4147 // Branch pruning: we adapt the output item to the context (it will
4148 // be used to limit its computation)
4149 $this->branchPruner->initialiseForLoop();
4150
4151 $opCharacter = $formula[$index]; // Get the first character of the value at the current index position
4152
4153 // Check for two-character operators (e.g. >=, <=, <>)
4154 if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
4155 $opCharacter .= $formula[++$index];
4156 }
4157 // Find out if we're currently at the beginning of a number, variable, cell/row/column reference,
4158 // function, defined name, structured reference, parenthesis, error or operand
4159 $isOperandOrFunction = (bool) preg_match($regexpMatchString, substr($formula, $index), $match);
4160
4161 $expectingOperatorCopy = $expectingOperator;
4162 if ($opCharacter === '-' && !$expectingOperator) { // Is it a negation instead of a minus?
4163 // Put a negation on the stack
4164 $stack->push('Unary Operator', '~');
4165 ++$index; // and drop the negation symbol
4166 } elseif ($opCharacter === '%' && $expectingOperator) {
4167 // Put a percentage on the stack
4168 $stack->push('Unary Operator', '%');
4169 ++$index;
4170 } elseif ($opCharacter === '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded?
4171 ++$index; // Drop the redundant plus symbol
4172 } elseif ((($opCharacter === '~') || ($opCharacter === '') || ($opCharacter === '')) && (!$isOperandOrFunction)) {
4173 // We have to explicitly deny a tilde, union or intersect because they are legal
4174 return $this->raiseFormulaError("Formula Error: Illegal character '~'"); // on the stack but not in the input expression
4175 } elseif ((isset(self::CALCULATION_OPERATORS[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) { // Are we putting an operator on the stack?
4176 while (
4177 $stack->count() > 0 &&
4178 ($o2 = $stack->last()) &&
4179 isset(self::CALCULATION_OPERATORS[$o2['value']]) &&
4180 @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
4181 ) {
4182 $output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output
4183 }
4184
4185 // Finally put our current operator onto the stack
4186 $stack->push('Binary Operator', $opCharacter);
4187
4188 ++$index;
4189 $expectingOperator = false;
4190 } elseif ($opCharacter === ')' && $expectingOperator) { // Are we expecting to close a parenthesis?
4191 $expectingOperand = false;
4192 while (($o2 = $stack->pop()) && $o2['value'] !== '(') { // Pop off the stack back to the last (
4193 $output[] = $o2;
4194 }
4195 $d = $stack->last(2);
4196
4197 // Branch pruning we decrease the depth whether is it a function
4198 // call or a parenthesis
4199 $this->branchPruner->decrementDepth();
4200
4201 if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) {
4202 // Did this parenthesis just close a function?
4203 try {
4204 $this->branchPruner->closingBrace($d['value']);
4205 } catch (Exception $e) {
4206 return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
4207 }
4208
4209 $functionName = $matches[1]; // Get the function name
4210 $d = $stack->pop();
4211 $argumentCount = $d['value'] ?? 0; // See how many arguments there were (argument count is the next value stored on the stack)
4212 $output[] = $d; // Dump the argument count on the output
4213 $output[] = $stack->pop(); // Pop the function and push onto the output
4214 if (isset(self::$controlFunctions[$functionName])) {
4215 $expectedArgumentCount = self::$controlFunctions[$functionName]['argumentCount'];
4216 // Scrutinizer says functionCall is unused after this assignment.
4217 // It might be right, but I'm too lazy to confirm.
4218 $functionCall = self::$controlFunctions[$functionName]['functionCall'];
4219 self::doNothing($functionCall);
4220 } elseif (isset(self::$phpSpreadsheetFunctions[$functionName])) {
4221 $expectedArgumentCount = self::$phpSpreadsheetFunctions[$functionName]['argumentCount'];
4222 $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
4223 self::doNothing($functionCall);
4224 } else { // did we somehow push a non-function on the stack? this should never happen
4225 return $this->raiseFormulaError('Formula Error: Internal error, non-function on stack');
4226 }
4227 // Check the argument count
4228 $argumentCountError = false;
4229 $expectedArgumentCountString = null;
4230 if (is_numeric($expectedArgumentCount)) {
4231 if ($expectedArgumentCount < 0) {
4232 if ($argumentCount > abs($expectedArgumentCount)) {
4233 $argumentCountError = true;
4234 $expectedArgumentCountString = 'no more than ' . abs($expectedArgumentCount);
4235 }
4236 } else {
4237 if ($argumentCount != $expectedArgumentCount) {
4238 $argumentCountError = true;
4239 $expectedArgumentCountString = $expectedArgumentCount;
4240 }
4241 }
4242 } elseif ($expectedArgumentCount != '*') {
4243 $isOperandOrFunction = preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch);
4244 self::doNothing($isOperandOrFunction);
4245 switch ($argMatch[2] ?? '') {
4246 case '+':
4247 if ($argumentCount < $argMatch[1]) {
4248 $argumentCountError = true;
4249 $expectedArgumentCountString = $argMatch[1] . ' or more ';
4250 }
4251
4252 break;
4253 case '-':
4254 if (($argumentCount < $argMatch[1]) || ($argumentCount > $argMatch[3])) {
4255 $argumentCountError = true;
4256 $expectedArgumentCountString = 'between ' . $argMatch[1] . ' and ' . $argMatch[3];
4257 }
4258
4259 break;
4260 case ',':
4261 if (($argumentCount != $argMatch[1]) && ($argumentCount != $argMatch[3])) {
4262 $argumentCountError = true;
4263 $expectedArgumentCountString = 'either ' . $argMatch[1] . ' or ' . $argMatch[3];
4264 }
4265
4266 break;
4267 }
4268 }
4269 if ($argumentCountError) {
4270 return $this->raiseFormulaError("Formula Error: Wrong number of arguments for $functionName() function: $argumentCount given, " . $expectedArgumentCountString . ' expected');
4271 }
4272 }
4273 ++$index;
4274 } elseif ($opCharacter === ',') { // Is this the separator for function arguments?
4275 try {
4276 $this->branchPruner->argumentSeparator();
4277 } catch (Exception $e) {
4278 return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
4279 }
4280
4281 while (($o2 = $stack->pop()) && $o2['value'] !== '(') { // Pop off the stack back to the last (
4282 $output[] = $o2; // pop the argument expression stuff and push onto the output
4283 }
4284 // If we've a comma when we're expecting an operand, then what we actually have is a null operand;
4285 // so push a null onto the stack
4286 if (($expectingOperand) || (!$expectingOperator)) {
4287 $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => 'NULL'];
4288 }
4289 // make sure there was a function
4290 $d = $stack->last(2);
4291 if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'] ?? '', $matches)) {
4292 // Can we inject a dummy function at this point so that the braces at least have some context
4293 // because at least the braces are paired up (at this stage in the formula)
4294 // MS Excel allows this if the content is cell references; but doesn't allow actual values,
4295 // but at this point, we can't differentiate (so allow both)
4296 return $this->raiseFormulaError('Formula Error: Unexpected ,');
4297 }
4298
4299 /** @var array $d */
4300 $d = $stack->pop();
4301 ++$d['value']; // increment the argument count
4302
4303 $stack->pushStackItem($d);
4304 $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again
4305
4306 $expectingOperator = false;
4307 $expectingOperand = true;
4308 ++$index;
4309 } elseif ($opCharacter === '(' && !$expectingOperator) {
4310 // Branch pruning: we go deeper
4311 $this->branchPruner->incrementDepth();
4312 $stack->push('Brace', '(', null);
4313 ++$index;
4314 } elseif ($isOperandOrFunction && !$expectingOperatorCopy) {
4315 // do we now have a function/variable/number?
4316 $expectingOperator = true;
4317 $expectingOperand = false;
4318 $val = $match[1];
4319 $length = strlen($val);
4320
4321 if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
4322 $val = (string) preg_replace('/\s/u', '', $val);
4323 if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
4324 $valToUpper = strtoupper($val);
4325 } else {
4326 $valToUpper = 'NAME.ERROR(';
4327 }
4328 // here $matches[1] will contain values like "IF"
4329 // and $val "IF("
4330
4331 $this->branchPruner->functionCall($valToUpper);
4332
4333 $stack->push('Function', $valToUpper);
4334 // tests if the function is closed right after opening
4335 $ax = preg_match('/^\s*\)/u', substr($formula, $index + $length));
4336 if ($ax) {
4337 $stack->push('Operand Count for Function ' . $valToUpper . ')', 0);
4338 $expectingOperator = true;
4339 } else {
4340 $stack->push('Operand Count for Function ' . $valToUpper . ')', 1);
4341 $expectingOperator = false;
4342 }
4343 $stack->push('Brace', '(');
4344 } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $val, $matches)) {
4345 // Watch for this case-change when modifying to allow cell references in different worksheets...
4346 // Should only be applied to the actual cell column, not the worksheet name
4347 // If the last entry on the stack was a : operator, then we have a cell range reference
4348 $testPrevOp = $stack->last(1);
4349 if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
4350 // If we have a worksheet reference, then we're playing with a 3D reference
4351 if ($matches[2] === '') {
4352 // Otherwise, we 'inherit' the worksheet reference from the start cell reference
4353 // The start of the cell range reference should be the last entry in $output
4354 $rangeStartCellRef = $output[count($output) - 1]['value'] ?? '';
4355 if ($rangeStartCellRef === ':') {
4356 // Do we have chained range operators?
4357 $rangeStartCellRef = $output[count($output) - 2]['value'] ?? '';
4358 }
4359 preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
4360 if (array_key_exists(2, $rangeStartMatches)) {
4361 if ($rangeStartMatches[2] > '') {
4362 $val = $rangeStartMatches[2] . '!' . $val;
4363 }
4364 } else {
4365 $val = Information\ExcelError::REF();
4366 }
4367 } else {
4368 $rangeStartCellRef = $output[count($output) - 1]['value'] ?? '';
4369 if ($rangeStartCellRef === ':') {
4370 // Do we have chained range operators?
4371 $rangeStartCellRef = $output[count($output) - 2]['value'] ?? '';
4372 }
4373 preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches);
4374 if ($rangeStartMatches[2] !== $matches[2]) {
4375 return $this->raiseFormulaError('3D Range references are not yet supported');
4376 }
4377 }
4378 } elseif (strpos($val, '!') === false && $pCellParent !== null) {
4379 $worksheet = $pCellParent->getTitle();
4380 $val = "'{$worksheet}'!{$val}";
4381 }
4382 // unescape any apostrophes or double quotes in worksheet name
4383 $val = str_replace(["''", '""'], ["'", '"'], $val);
4384 $outputItem = $stack->getStackItem('Cell Reference', $val, $val);
4385
4386 $output[] = $outputItem;
4387 } elseif (preg_match('/^' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE . '$/miu', $val, $matches)) {
4388 try {
4389 $structuredReference = Operands\StructuredReference::fromParser($formula, $index, $matches);
4390 } catch (Exception $e) {
4391 return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
4392 }
4393
4394 $val = $structuredReference->value();
4395 $length = strlen($val);
4396 $outputItem = $stack->getStackItem(Operands\StructuredReference::NAME, $structuredReference, null);
4397
4398 $output[] = $outputItem;
4399 $expectingOperator = true;
4400 } else {
4401 // it's a variable, constant, string, number or boolean
4402 $localeConstant = false;
4403 $stackItemType = 'Value';
4404 $stackItemReference = null;
4405
4406 // If the last entry on the stack was a : operator, then we may have a row or column range reference
4407 $testPrevOp = $stack->last(1);
4408 if ($testPrevOp !== null && $testPrevOp['value'] === ':') {
4409 $stackItemType = 'Cell Reference';
4410
4411 if (
4412 !is_numeric($val) &&
4413 ((ctype_alpha($val) === false || strlen($val) > 3)) &&
4414 (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $val) !== false) &&
4415 ($this->spreadsheet === null || $this->spreadsheet->getNamedRange($val) !== null)
4416 ) {
4417 $namedRange = ($this->spreadsheet === null) ? null : $this->spreadsheet->getNamedRange($val);
4418 if ($namedRange !== null) {
4419 $stackItemType = 'Defined Name';
4420 $address = str_replace('$', '', $namedRange->getValue());
4421 $stackItemReference = $val;
4422 if (strpos($address, ':') !== false) {
4423 // We'll need to manipulate the stack for an actual named range rather than a named cell
4424 $fromTo = explode(':', $address);
4425 $to = array_pop($fromTo);
4426 foreach ($fromTo as $from) {
4427 $output[] = $stack->getStackItem($stackItemType, $from, $stackItemReference);
4428 $output[] = $stack->getStackItem('Binary Operator', ':');
4429 }
4430 $address = $to;
4431 }
4432 $val = $address;
4433 }
4434 } elseif ($val === Information\ExcelError::REF()) {
4435 $stackItemReference = $val;
4436 } else {
4437 $startRowColRef = $output[count($output) - 1]['value'] ?? '';
4438 [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true);
4439 $rangeSheetRef = $rangeWS1;
4440 if ($rangeWS1 !== '') {
4441 $rangeWS1 .= '!';
4442 }
4443 $rangeSheetRef = trim($rangeSheetRef, "'");
4444 [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true);
4445 if ($rangeWS2 !== '') {
4446 $rangeWS2 .= '!';
4447 } else {
4448 $rangeWS2 = $rangeWS1;
4449 }
4450
4451 $refSheet = $pCellParent;
4452 if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) {
4453 $refSheet = $pCellParent->getParentOrThrow()->getSheetByName($rangeSheetRef);
4454 }
4455
4456 if (ctype_digit($val) && $val <= 1048576) {
4457 // Row range
4458 $stackItemType = 'Row Reference';
4459 /** @var int $valx */
4460 $valx = $val;
4461 $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : AddressRange::MAX_COLUMN; // Max 16,384 columns for Excel2007
4462 $val = "{$rangeWS2}{$endRowColRef}{$val}";
4463 } elseif (ctype_alpha($val) && strlen($val) <= 3) {
4464 // Column range
4465 $stackItemType = 'Column Reference';
4466 $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
4467 $val = "{$rangeWS2}{$val}{$endRowColRef}";
4468 }
4469 $stackItemReference = $val;
4470 }
4471 } elseif ($opCharacter === self::FORMULA_STRING_QUOTE) {
4472 // UnEscape any quotes within the string
4473 $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val)));
4474 } elseif (isset(self::$excelConstants[trim(strtoupper($val))])) {
4475 $stackItemType = 'Constant';
4476 $excelConstant = trim(strtoupper($val));
4477 $val = self::$excelConstants[$excelConstant];
4478 $stackItemReference = $excelConstant;
4479 } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
4480 $stackItemType = 'Constant';
4481 $val = self::$excelConstants[$localeConstant];
4482 $stackItemReference = $localeConstant;
4483 } elseif (
4484 preg_match('/^' . self::CALCULATION_REGEXP_ROW_RANGE . '/miu', substr($formula, $index), $rowRangeReference)
4485 ) {
4486 $val = $rowRangeReference[1];
4487 $length = strlen($rowRangeReference[1]);
4488 $stackItemType = 'Row Reference';
4489 // unescape any apostrophes or double quotes in worksheet name
4490 $val = str_replace(["''", '""'], ["'", '"'], $val);
4491 $column = 'A';
4492 if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
4493 $column = $pCellParent->getHighestDataColumn($val);
4494 }
4495 $val = "{$rowRangeReference[2]}{$column}{$rowRangeReference[7]}";
4496 $stackItemReference = $val;
4497 } elseif (
4498 preg_match('/^' . self::CALCULATION_REGEXP_COLUMN_RANGE . '/miu', substr($formula, $index), $columnRangeReference)
4499 ) {
4500 $val = $columnRangeReference[1];
4501 $length = strlen($val);
4502 $stackItemType = 'Column Reference';
4503 // unescape any apostrophes or double quotes in worksheet name
4504 $val = str_replace(["''", '""'], ["'", '"'], $val);
4505 $row = '1';
4506 if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
4507 $row = $pCellParent->getHighestDataRow($val);
4508 }
4509 $val = "{$val}{$row}";
4510 $stackItemReference = $val;
4511 } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', $val, $match)) {
4512 $stackItemType = 'Defined Name';
4513 $stackItemReference = $val;
4514 } elseif (is_numeric($val)) {
4515 if ((strpos((string) $val, '.') !== false) || (stripos((string) $val, 'e') !== false) || ($val > PHP_INT_MAX) || ($val < -PHP_INT_MAX)) {
4516 $val = (float) $val;
4517 } else {
4518 $val = (int) $val;
4519 }
4520 }
4521
4522 $details = $stack->getStackItem($stackItemType, $val, $stackItemReference);
4523 if ($localeConstant) {
4524 $details['localeValue'] = $localeConstant;
4525 }
4526 $output[] = $details;
4527 }
4528 $index += $length;
4529 } elseif ($opCharacter === '$') { // absolute row or column range
4530 ++$index;
4531 } elseif ($opCharacter === ')') { // miscellaneous error checking
4532 if ($expectingOperand) {
4533 $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => 'NULL'];
4534 $expectingOperand = false;
4535 $expectingOperator = true;
4536 } else {
4537 return $this->raiseFormulaError("Formula Error: Unexpected ')'");
4538 }
4539 } elseif (isset(self::CALCULATION_OPERATORS[$opCharacter]) && !$expectingOperator) {
4540 return $this->raiseFormulaError("Formula Error: Unexpected operator '$opCharacter'");
4541 } else { // I don't even want to know what you did to get here
4542 return $this->raiseFormulaError('Formula Error: An unexpected error occurred');
4543 }
4544 // Test for end of formula string
4545 if ($index == strlen($formula)) {
4546 // Did we end with an operator?.
4547 // Only valid for the % unary operator
4548 if ((isset(self::CALCULATION_OPERATORS[$opCharacter])) && ($opCharacter != '%')) {
4549 return $this->raiseFormulaError("Formula Error: Operator '$opCharacter' has no operands");
4550 }
4551
4552 break;
4553 }
4554 // Ignore white space
4555 while (($formula[$index] === "\n") || ($formula[$index] === "\r")) {
4556 ++$index;
4557 }
4558
4559 if ($formula[$index] === ' ') {
4560 while ($formula[$index] === ' ') {
4561 ++$index;
4562 }
4563
4564 // If we're expecting an operator, but only have a space between the previous and next operands (and both are
4565 // Cell References, Defined Names or Structured References) then we have an INTERSECTION operator
4566 $countOutputMinus1 = count($output) - 1;
4567 if (
4568 ($expectingOperator) &&
4569 array_key_exists($countOutputMinus1, $output) &&
4570 is_array($output[$countOutputMinus1]) &&
4571 array_key_exists('type', $output[$countOutputMinus1]) &&
4572 (
4573 (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/miu', substr($formula, $index), $match)) &&
4574 ($output[$countOutputMinus1]['type'] === 'Cell Reference') ||
4575 (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', substr($formula, $index), $match)) &&
4576 ($output[$countOutputMinus1]['type'] === 'Defined Name' || $output[$countOutputMinus1]['type'] === 'Value') ||
4577 (preg_match('/^' . self::CALCULATION_REGEXP_STRUCTURED_REFERENCE . '.*/miu', substr($formula, $index), $match)) &&
4578 ($output[$countOutputMinus1]['type'] === Operands\StructuredReference::NAME || $output[$countOutputMinus1]['type'] === 'Value')
4579 )
4580 ) {
4581 while (
4582 $stack->count() > 0 &&
4583 ($o2 = $stack->last()) &&
4584 isset(self::CALCULATION_OPERATORS[$o2['value']]) &&
4585 @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])
4586 ) {
4587 $output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output
4588 }
4589 $stack->push('Binary Operator', ''); // Put an Intersect Operator on the stack
4590 $expectingOperator = false;
4591 }
4592 }
4593 }
4594
4595 while (($op = $stack->pop()) !== null) {
4596 // pop everything off the stack and push onto output
4597 if ((is_array($op) && $op['value'] == '(')) {
4598 return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced
4599 }
4600 $output[] = $op;
4601 }
4602
4603 return $output;
4604 }
4605
4606 /**
4607 * @param array $operandData
4608 *
4609 * @return mixed
4610 */
4611 private static function dataTestReference(&$operandData)
4612 {
4613 $operand = $operandData['value'];
4614 if (($operandData['reference'] === null) && (is_array($operand))) {
4615 $rKeys = array_keys($operand);
4616 $rowKey = array_shift($rKeys);
4617 if (is_array($operand[$rowKey]) === false) {
4618 $operandData['value'] = $operand[$rowKey];
4619
4620 return $operand[$rowKey];
4621 }
4622
4623 $cKeys = array_keys(array_keys($operand[$rowKey]));
4624 $colKey = array_shift($cKeys);
4625 if (ctype_upper("$colKey")) {
4626 $operandData['reference'] = $colKey . $rowKey;
4627 }
4628 }
4629
4630 return $operand;
4631 }
4632
4633 /**
4634 * @param mixed $tokens
4635 * @param null|string $cellID
4636 *
4637 * @return array<int, mixed>|false
4638 */
4639 private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
4640 {
4641 if ($tokens === false) {
4642 return false;
4643 }
4644
4645 // If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent cell collection),
4646 // so we store the parent cell collection so that we can re-attach it when necessary
4647 $pCellWorksheet = ($cell !== null) ? $cell->getWorksheet() : null;
4648 $pCellParent = ($cell !== null) ? $cell->getParent() : null;
4649 $stack = new Stack($this->branchPruner);
4650
4651 // Stores branches that have been pruned
4652 $fakedForBranchPruning = [];
4653 // help us to know when pruning ['branchTestId' => true/false]
4654 $branchStore = [];
4655 // Loop through each token in turn
4656 foreach ($tokens as $tokenData) {
4657 $token = $tokenData['value'];
4658 // Branch pruning: skip useless resolutions
4659 $storeKey = $tokenData['storeKey'] ?? null;
4660 if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) {
4661 $onlyIfStoreKey = $tokenData['onlyIf'];
4662 $storeValue = $branchStore[$onlyIfStoreKey] ?? null;
4663 $storeValueAsBool = ($storeValue === null) ?
4664 true : (bool) Functions::flattenSingleValue($storeValue);
4665 if (is_array($storeValue)) {
4666 $wrappedItem = end($storeValue);
4667 $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem;
4668 }
4669
4670 if (
4671 (isset($storeValue) || $tokenData['reference'] === 'NULL')
4672 && (!$storeValueAsBool || Information\ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch'))
4673 ) {
4674 // If branching value is not true, we don't need to compute
4675 if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) {
4676 $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token);
4677 $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true;
4678 }
4679
4680 if (isset($storeKey)) {
4681 // We are processing an if condition
4682 // We cascade the pruning to the depending branches
4683 $branchStore[$storeKey] = 'Pruned branch';
4684 $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4685 $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4686 }
4687
4688 continue;
4689 }
4690 }
4691
4692 if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) {
4693 $onlyIfNotStoreKey = $tokenData['onlyIfNot'];
4694 $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null;
4695 $storeValueAsBool = ($storeValue === null) ?
4696 true : (bool) Functions::flattenSingleValue($storeValue);
4697 if (is_array($storeValue)) {
4698 $wrappedItem = end($storeValue);
4699 $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem;
4700 }
4701
4702 if (
4703 (isset($storeValue) || $tokenData['reference'] === 'NULL')
4704 && ($storeValueAsBool || Information\ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch'))
4705 ) {
4706 // If branching value is true, we don't need to compute
4707 if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) {
4708 $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token);
4709 $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true;
4710 }
4711
4712 if (isset($storeKey)) {
4713 // We are processing an if condition
4714 // We cascade the pruning to the depending branches
4715 $branchStore[$storeKey] = 'Pruned branch';
4716 $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
4717 $fakedForBranchPruning['onlyIf-' . $storeKey] = true;
4718 }
4719
4720 continue;
4721 }
4722 }
4723
4724 if ($token instanceof Operands\StructuredReference) {
4725 if ($cell === null) {
4726 return $this->raiseFormulaError('Structured References must exist in a Cell context');
4727 }
4728
4729 try {
4730 $cellRange = $token->parse($cell);
4731 if (strpos($cellRange, ':') !== false) {
4732 $this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell Range %s', $token->value(), $cellRange);
4733 $rangeValue = self::getInstance($cell->getWorksheet()->getParent())->_calculateFormulaValue("={$cellRange}", $cellRange, $cell);
4734 $stack->push('Value', $rangeValue);
4735 $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as value %s', $token->value(), $this->showValue($rangeValue));
4736 } else {
4737 $this->debugLog->writeDebugLog('Evaluating Structured Reference %s as Cell %s', $token->value(), $cellRange);
4738 $cellValue = $cell->getWorksheet()->getCell($cellRange)->getCalculatedValue(false);
4739 $stack->push('Cell Reference', $cellValue, $cellRange);
4740 $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as value %s', $token->value(), $this->showValue($cellValue));
4741 }
4742 } catch (Exception $e) {
4743 if ($e->getCode() === Exception::CALCULATION_ENGINE_PUSH_TO_STACK) {
4744 $stack->push('Error', Information\ExcelError::REF(), null);
4745 $this->debugLog->writeDebugLog('Evaluated Structured Reference %s as error value %s', $token->value(), Information\ExcelError::REF());
4746 } else {
4747 return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
4748 }
4749 }
4750 } elseif (!is_numeric($token) && !is_object($token) && isset(self::BINARY_OPERATORS[$token])) {
4751 // 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
4752 // We must have two operands, error if we don't
4753 if (($operand2Data = $stack->pop()) === null) {
4754 return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4755 }
4756 if (($operand1Data = $stack->pop()) === null) {
4757 return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4758 }
4759
4760 $operand1 = self::dataTestReference($operand1Data);
4761 $operand2 = self::dataTestReference($operand2Data);
4762
4763 // Log what we're doing
4764 if ($token == ':') {
4765 $this->debugLog->writeDebugLog('Evaluating Range %s %s %s', $this->showValue($operand1Data['reference']), $token, $this->showValue($operand2Data['reference']));
4766 } else {
4767 $this->debugLog->writeDebugLog('Evaluating %s %s %s', $this->showValue($operand1), $token, $this->showValue($operand2));
4768 }
4769
4770 // Process the operation in the appropriate manner
4771 switch ($token) {
4772 // Comparison (Boolean) Operators
4773 case '>': // Greater than
4774 case '<': // Less than
4775 case '>=': // Greater than or Equal to
4776 case '<=': // Less than or Equal to
4777 case '=': // Equality
4778 case '<>': // Inequality
4779 $result = $this->executeBinaryComparisonOperation($operand1, $operand2, (string) $token, $stack);
4780 if (isset($storeKey)) {
4781 $branchStore[$storeKey] = $result;
4782 }
4783
4784 break;
4785 // Binary Operators
4786 case ':': // Range
4787 if ($operand1Data['type'] === 'Defined Name') {
4788 if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false && $this->spreadsheet !== null) {
4789 $definedName = $this->spreadsheet->getNamedRange($operand1Data['reference']);
4790 if ($definedName !== null) {
4791 $operand1Data['reference'] = $operand1Data['value'] = str_replace('$', '', $definedName->getValue());
4792 }
4793 }
4794 }
4795 if (strpos($operand1Data['reference'] ?? '', '!') !== false) {
4796 [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true);
4797 } else {
4798 $sheet1 = ($pCellWorksheet !== null) ? $pCellWorksheet->getTitle() : '';
4799 }
4800
4801 [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true);
4802 if (empty($sheet2)) {
4803 $sheet2 = $sheet1;
4804 }
4805
4806 if (trim($sheet1, "'") === trim($sheet2, "'")) {
4807 if ($operand1Data['reference'] === null && $cell !== null) {
4808 if (is_array($operand1Data['value'])) {
4809 $operand1Data['reference'] = $cell->getCoordinate();
4810 } elseif ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) {
4811 $operand1Data['reference'] = $cell->getColumn() . $operand1Data['value'];
4812 } elseif (trim($operand1Data['value']) == '') {
4813 $operand1Data['reference'] = $cell->getCoordinate();
4814 } else {
4815 $operand1Data['reference'] = $operand1Data['value'] . $cell->getRow();
4816 }
4817 }
4818 if ($operand2Data['reference'] === null && $cell !== null) {
4819 if (is_array($operand2Data['value'])) {
4820 $operand2Data['reference'] = $cell->getCoordinate();
4821 } elseif ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) {
4822 $operand2Data['reference'] = $cell->getColumn() . $operand2Data['value'];
4823 } elseif (trim($operand2Data['value']) == '') {
4824 $operand2Data['reference'] = $cell->getCoordinate();
4825 } else {
4826 $operand2Data['reference'] = $operand2Data['value'] . $cell->getRow();
4827 }
4828 }
4829
4830 $oData = array_merge(explode(':', $operand1Data['reference']), explode(':', $operand2Data['reference']));
4831 $oCol = $oRow = [];
4832 $breakNeeded = false;
4833 foreach ($oData as $oDatum) {
4834 try {
4835 $oCR = Coordinate::coordinateFromString($oDatum);
4836 $oCol[] = Coordinate::columnIndexFromString($oCR[0]) - 1;
4837 $oRow[] = $oCR[1];
4838 } catch (\Exception $e) {
4839 $stack->push('Error', Information\ExcelError::REF(), null);
4840 $breakNeeded = true;
4841
4842 break;
4843 }
4844 }
4845 if ($breakNeeded) {
4846 break;
4847 }
4848 $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4849 if ($pCellParent !== null && $this->spreadsheet !== null) {
4850 $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
4851 } else {
4852 return $this->raiseFormulaError('Unable to access Cell Reference');
4853 }
4854
4855 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellValue));
4856 $stack->push('Cell Reference', $cellValue, $cellRef);
4857 } else {
4858 $this->debugLog->writeDebugLog('Evaluation Result is a #REF! Error');
4859 $stack->push('Error', Information\ExcelError::REF(), null);
4860 }
4861
4862 break;
4863 case '+': // Addition
4864 case '-': // Subtraction
4865 case '*': // Multiplication
4866 case '/': // Division
4867 case '^': // Exponential
4868 $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, $stack);
4869 if (isset($storeKey)) {
4870 $branchStore[$storeKey] = $result;
4871 }
4872
4873 break;
4874 case '&': // Concatenation
4875 // If either of the operands is a matrix, we need to treat them both as matrices
4876 // (converting the other operand to a matrix if need be); then perform the required
4877 // matrix operation
4878 $operand1 = self::boolToString($operand1);
4879 $operand2 = self::boolToString($operand2);
4880 if (is_array($operand1) || is_array($operand2)) {
4881 if (is_string($operand1)) {
4882 $operand1 = self::unwrapResult($operand1);
4883 }
4884 if (is_string($operand2)) {
4885 $operand2 = self::unwrapResult($operand2);
4886 }
4887 // Ensure that both operands are arrays/matrices
4888 [$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 2);
4889
4890 for ($row = 0; $row < $rows; ++$row) {
4891 for ($column = 0; $column < $columns; ++$column) {
4892 $operand1[$row][$column] =
4893 Shared\StringHelper::substring(
4894 self::boolToString($operand1[$row][$column])
4895 . self::boolToString($operand2[$row][$column]),
4896 0,
4897 DataType::MAX_STRING_LENGTH
4898 );
4899 }
4900 }
4901 $result = $operand1;
4902 } else {
4903 // In theory, we should truncate here.
4904 // But I can't figure out a formula
4905 // using the concatenation operator
4906 // with literals that fits in 32K,
4907 // so I don't think we can overflow here.
4908 $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE;
4909 }
4910 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
4911 $stack->push('Value', $result);
4912
4913 if (isset($storeKey)) {
4914 $branchStore[$storeKey] = $result;
4915 }
4916
4917 break;
4918 case '': // Intersect
4919 $rowIntersect = array_intersect_key($operand1, $operand2);
4920 $cellIntersect = $oCol = $oRow = [];
4921 foreach (array_keys($rowIntersect) as $row) {
4922 $oRow[] = $row;
4923 foreach ($rowIntersect[$row] as $col => $data) {
4924 $oCol[] = Coordinate::columnIndexFromString($col) - 1;
4925 $cellIntersect[$row] = array_intersect_key($operand1[$row], $operand2[$row]);
4926 }
4927 }
4928 if (count(Functions::flattenArray($cellIntersect)) === 0) {
4929 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
4930 $stack->push('Error', Information\ExcelError::null(), null);
4931 } else {
4932 $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' .
4933 Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
4934 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
4935 $stack->push('Value', $cellIntersect, $cellRef);
4936 }
4937
4938 break;
4939 }
4940 } elseif (($token === '~') || ($token === '%')) {
4941 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
4942 if (($arg = $stack->pop()) === null) {
4943 return $this->raiseFormulaError('Internal error - Operand value missing from stack');
4944 }
4945 $arg = $arg['value'];
4946 if ($token === '~') {
4947 $this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg));
4948 $multiplier = -1;
4949 } else {
4950 $this->debugLog->writeDebugLog('Evaluating Percentile of %s', $this->showValue($arg));
4951 $multiplier = 0.01;
4952 }
4953 if (is_array($arg)) {
4954 $operand2 = $multiplier;
4955 $result = $arg;
4956 [$rows, $columns] = self::checkMatrixOperands($result, $operand2, 0);
4957 for ($row = 0; $row < $rows; ++$row) {
4958 for ($column = 0; $column < $columns; ++$column) {
4959 if (self::isNumericOrBool($result[$row][$column])) {
4960 $result[$row][$column] *= $multiplier;
4961 } else {
4962 $result[$row][$column] = self::makeError($result[$row][$column]);
4963 }
4964 }
4965 }
4966
4967 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
4968 $stack->push('Value', $result);
4969 if (isset($storeKey)) {
4970 $branchStore[$storeKey] = $result;
4971 }
4972 } else {
4973 $this->executeNumericBinaryOperation($multiplier, $arg, '*', $stack);
4974 }
4975 } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token ?? '', $matches)) {
4976 $cellRef = null;
4977
4978 if (isset($matches[8])) {
4979 if ($cell === null) {
4980 // We can't access the range, so return a REF error
4981 $cellValue = Information\ExcelError::REF();
4982 } else {
4983 $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10];
4984 if ($matches[2] > '') {
4985 $matches[2] = trim($matches[2], "\"'");
4986 if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
4987 // It's a Reference to an external spreadsheet (not currently supported)
4988 return $this->raiseFormulaError('Unable to access External Workbook');
4989 }
4990 $matches[2] = trim($matches[2], "\"'");
4991 $this->debugLog->writeDebugLog('Evaluating Cell Range %s in worksheet %s', $cellRef, $matches[2]);
4992 if ($pCellParent !== null && $this->spreadsheet !== null) {
4993 $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
4994 } else {
4995 return $this->raiseFormulaError('Unable to access Cell Reference');
4996 }
4997 $this->debugLog->writeDebugLog('Evaluation Result for cells %s in worksheet %s is %s', $cellRef, $matches[2], $this->showTypeDetails($cellValue));
4998 } else {
4999 $this->debugLog->writeDebugLog('Evaluating Cell Range %s in current worksheet', $cellRef);
5000 if ($pCellParent !== null) {
5001 $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
5002 } else {
5003 return $this->raiseFormulaError('Unable to access Cell Reference');
5004 }
5005 $this->debugLog->writeDebugLog('Evaluation Result for cells %s is %s', $cellRef, $this->showTypeDetails($cellValue));
5006 }
5007 }
5008 } else {
5009 if ($cell === null) {
5010 // We can't access the cell, so return a REF error
5011 $cellValue = Information\ExcelError::REF();
5012 } else {
5013 $cellRef = $matches[6] . $matches[7];
5014 if ($matches[2] > '') {
5015 $matches[2] = trim($matches[2], "\"'");
5016 if ((strpos($matches[2], '[') !== false) || (strpos($matches[2], ']') !== false)) {
5017 // It's a Reference to an external spreadsheet (not currently supported)
5018 return $this->raiseFormulaError('Unable to access External Workbook');
5019 }
5020 $this->debugLog->writeDebugLog('Evaluating Cell %s in worksheet %s', $cellRef, $matches[2]);
5021 if ($pCellParent !== null && $this->spreadsheet !== null) {
5022 $cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
5023 if ($cellSheet && $cellSheet->cellExists($cellRef)) {
5024 $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
5025 $cell->attach($pCellParent);
5026 } else {
5027 $cellRef = ($cellSheet !== null) ? "'{$matches[2]}'!{$cellRef}" : $cellRef;
5028 $cellValue = ($cellSheet !== null) ? null : Information\ExcelError::REF();
5029 }
5030 } else {
5031 return $this->raiseFormulaError('Unable to access Cell Reference');
5032 }
5033 $this->debugLog->writeDebugLog('Evaluation Result for cell %s in worksheet %s is %s', $cellRef, $matches[2], $this->showTypeDetails($cellValue));
5034 } else {
5035 $this->debugLog->writeDebugLog('Evaluating Cell %s in current worksheet', $cellRef);
5036 if ($pCellParent !== null && $pCellParent->has($cellRef)) {
5037 $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false);
5038 $cell->attach($pCellParent);
5039 } else {
5040 $cellValue = null;
5041 }
5042 $this->debugLog->writeDebugLog('Evaluation Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue));
5043 }
5044 }
5045 }
5046
5047 $stack->push('Cell Value', $cellValue, $cellRef);
5048 if (isset($storeKey)) {
5049 $branchStore[$storeKey] = $cellValue;
5050 }
5051 } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) {
5052 // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
5053 if ($cell !== null && $pCellParent !== null) {
5054 $cell->attach($pCellParent);
5055 }
5056
5057 $functionName = $matches[1];
5058 $argCount = $stack->pop();
5059 $argCount = $argCount['value'];
5060 if ($functionName !== 'MKMATRIX') {
5061 $this->debugLog->writeDebugLog('Evaluating Function %s() with %s argument%s', self::localeFunc($functionName), (($argCount == 0) ? 'no' : $argCount), (($argCount == 1) ? '' : 's'));
5062 }
5063 if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) { // function
5064 $passByReference = false;
5065 $passCellReference = false;
5066 $functionCall = null;
5067 if (isset(self::$phpSpreadsheetFunctions[$functionName])) {
5068 $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall'];
5069 $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']);
5070 $passCellReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passCellReference']);
5071 } elseif (isset(self::$controlFunctions[$functionName])) {
5072 $functionCall = self::$controlFunctions[$functionName]['functionCall'];
5073 $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
5074 $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
5075 }
5076
5077 // get the arguments for this function
5078 $args = $argArrayVals = [];
5079 $emptyArguments = [];
5080 for ($i = 0; $i < $argCount; ++$i) {
5081 $arg = $stack->pop();
5082 $a = $argCount - $i - 1;
5083 if (
5084 ($passByReference) &&
5085 (isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) &&
5086 (self::$phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
5087 ) {
5088 if ($arg['reference'] === null) {
5089 $args[] = $cellID;
5090 if ($functionName !== 'MKMATRIX') {
5091 $argArrayVals[] = $this->showValue($cellID);
5092 }
5093 } else {
5094 $args[] = $arg['reference'];
5095 if ($functionName !== 'MKMATRIX') {
5096 $argArrayVals[] = $this->showValue($arg['reference']);
5097 }
5098 }
5099 } else {
5100 $emptyArguments[] = ($arg['type'] === 'Empty Argument');
5101 $args[] = self::unwrapResult($arg['value']);
5102 if ($functionName !== 'MKMATRIX') {
5103 $argArrayVals[] = $this->showValue($arg['value']);
5104 }
5105 }
5106 }
5107
5108 // Reverse the order of the arguments
5109 krsort($args);
5110 krsort($emptyArguments);
5111
5112 if ($argCount > 0) {
5113 $args = $this->addDefaultArgumentValues($functionCall, $args, $emptyArguments);
5114 }
5115
5116 if (($passByReference) && ($argCount == 0)) {
5117 $args[] = $cellID;
5118 $argArrayVals[] = $this->showValue($cellID);
5119 }
5120
5121 if ($functionName !== 'MKMATRIX') {
5122 if ($this->debugLog->getWriteDebugLog()) {
5123 krsort($argArrayVals);
5124 $this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)));
5125 }
5126 }
5127
5128 // Process the argument with the appropriate function call
5129 $args = $this->addCellReference($args, $passCellReference, $functionCall, $cell);
5130
5131 if (!is_array($functionCall)) {
5132 foreach ($args as &$arg) {
5133 $arg = Functions::flattenSingleValue($arg);
5134 }
5135 unset($arg);
5136 }
5137
5138 $result = call_user_func_array($functionCall, $args);
5139
5140 if ($functionName !== 'MKMATRIX') {
5141 $this->debugLog->writeDebugLog('Evaluation Result for %s() function call is %s', self::localeFunc($functionName), $this->showTypeDetails($result));
5142 }
5143 $stack->push('Value', self::wrapResult($result));
5144 if (isset($storeKey)) {
5145 $branchStore[$storeKey] = $result;
5146 }
5147 }
5148 } else {
5149 // if the token is a number, boolean, string or an Excel error, push it onto the stack
5150 if (isset(self::$excelConstants[strtoupper($token ?? '')])) {
5151 $excelConstant = strtoupper($token);
5152 $stack->push('Constant Value', self::$excelConstants[$excelConstant]);
5153 if (isset($storeKey)) {
5154 $branchStore[$storeKey] = self::$excelConstants[$excelConstant];
5155 }
5156 $this->debugLog->writeDebugLog('Evaluating Constant %s as %s', $excelConstant, $this->showTypeDetails(self::$excelConstants[$excelConstant]));
5157 } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) {
5158 $stack->push($tokenData['type'], $token, $tokenData['reference']);
5159 if (isset($storeKey)) {
5160 $branchStore[$storeKey] = $token;
5161 }
5162 } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) {
5163 // if the token is a named range or formula, evaluate it and push the result onto the stack
5164 $definedName = $matches[6];
5165 if ($cell === null || $pCellWorksheet === null) {
5166 return $this->raiseFormulaError("undefined name '$token'");
5167 }
5168
5169 $this->debugLog->writeDebugLog('Evaluating Defined Name %s', $definedName);
5170 $namedRange = DefinedName::resolveName($definedName, $pCellWorksheet);
5171 if ($namedRange === null) {
5172 return $this->raiseFormulaError("undefined name '$definedName'");
5173 }
5174
5175 $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack);
5176 if (isset($storeKey)) {
5177 $branchStore[$storeKey] = $result;
5178 }
5179 } else {
5180 return $this->raiseFormulaError("undefined name '$token'");
5181 }
5182 }
5183 }
5184 // when we're out of tokens, the stack should have a single element, the final result
5185 if ($stack->count() != 1) {
5186 return $this->raiseFormulaError('internal error');
5187 }
5188 $output = $stack->pop();
5189 $output = $output['value'];
5190
5191 return $output;
5192 }
5193
5194 /**
5195 * @param mixed $operand
5196 * @param mixed $stack
5197 *
5198 * @return bool
5199 */
5200 private function validateBinaryOperand(&$operand, &$stack)
5201 {
5202 if (is_array($operand)) {
5203 if ((count($operand, COUNT_RECURSIVE) - count($operand)) == 1) {
5204 do {
5205 $operand = array_pop($operand);
5206 } while (is_array($operand));
5207 }
5208 }
5209 // Numbers, matrices and booleans can pass straight through, as they're already valid
5210 if (is_string($operand)) {
5211 // We only need special validations for the operand if it is a string
5212 // Start by stripping off the quotation marks we use to identify true excel string values internally
5213 if ($operand > '' && $operand[0] == self::FORMULA_STRING_QUOTE) {
5214 $operand = self::unwrapResult($operand);
5215 }
5216 // If the string is a numeric value, we treat it as a numeric, so no further testing
5217 if (!is_numeric($operand)) {
5218 // If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations
5219 if ($operand > '' && $operand[0] == '#') {
5220 $stack->push('Value', $operand);
5221 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($operand));
5222
5223 return false;
5224 } elseif (Engine\FormattedNumber::convertToNumberIfFormatted($operand) === false) {
5225 // If not a numeric, a fraction or a percentage, then it's a text string, and so can't be used in mathematical binary operations
5226 $stack->push('Error', '#VALUE!');
5227 $this->debugLog->writeDebugLog('Evaluation Result is a %s', $this->showTypeDetails('#VALUE!'));
5228
5229 return false;
5230 }
5231 }
5232 }
5233
5234 // return a true if the value of the operand is one that we can use in normal binary mathematical operations
5235 return true;
5236 }
5237
5238 /**
5239 * @param mixed $operand1
5240 * @param mixed $operand2
5241 * @param string $operation
5242 *
5243 * @return array
5244 */
5245 private function executeArrayComparison($operand1, $operand2, $operation, Stack &$stack, bool $recursingArrays)
5246 {
5247 $result = [];
5248 if (!is_array($operand2)) {
5249 // Operand 1 is an array, Operand 2 is a scalar
5250 foreach ($operand1 as $x => $operandData) {
5251 $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operandData), $operation, $this->showValue($operand2));
5252 $this->executeBinaryComparisonOperation($operandData, $operand2, $operation, $stack);
5253 $r = $stack->pop();
5254 $result[$x] = $r['value'];
5255 }
5256 } elseif (!is_array($operand1)) {
5257 // Operand 1 is a scalar, Operand 2 is an array
5258 foreach ($operand2 as $x => $operandData) {
5259 $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operand1), $operation, $this->showValue($operandData));
5260 $this->executeBinaryComparisonOperation($operand1, $operandData, $operation, $stack);
5261 $r = $stack->pop();
5262 $result[$x] = $r['value'];
5263 }
5264 } else {
5265 // Operand 1 and Operand 2 are both arrays
5266 if (!$recursingArrays) {
5267 self::checkMatrixOperands($operand1, $operand2, 2);
5268 }
5269 foreach ($operand1 as $x => $operandData) {
5270 $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operandData), $operation, $this->showValue($operand2[$x]));
5271 $this->executeBinaryComparisonOperation($operandData, $operand2[$x], $operation, $stack, true);
5272 $r = $stack->pop();
5273 $result[$x] = $r['value'];
5274 }
5275 }
5276 // Log the result details
5277 $this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result));
5278 // And push the result onto the stack
5279 $stack->push('Array', $result);
5280
5281 return $result;
5282 }
5283
5284 /**
5285 * @param mixed $operand1
5286 * @param mixed $operand2
5287 * @param string $operation
5288 * @param bool $recursingArrays
5289 *
5290 * @return mixed
5291 */
5292 private function executeBinaryComparisonOperation($operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
5293 {
5294 // If we're dealing with matrix operations, we want a matrix result
5295 if ((is_array($operand1)) || (is_array($operand2))) {
5296 return $this->executeArrayComparison($operand1, $operand2, $operation, $stack, $recursingArrays);
5297 }
5298
5299 $result = BinaryComparison::compare($operand1, $operand2, $operation);
5300
5301 // Log the result details
5302 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
5303 // And push the result onto the stack
5304 $stack->push('Value', $result);
5305
5306 return $result;
5307 }
5308
5309 /**
5310 * @param mixed $operand1
5311 * @param mixed $operand2
5312 * @param string $operation
5313 * @param Stack $stack
5314 *
5315 * @return bool|mixed
5316 */
5317 private function executeNumericBinaryOperation($operand1, $operand2, $operation, &$stack)
5318 {
5319 // Validate the two operands
5320 if (
5321 ($this->validateBinaryOperand($operand1, $stack) === false) ||
5322 ($this->validateBinaryOperand($operand2, $stack) === false)
5323 ) {
5324 return false;
5325 }
5326
5327 if (
5328 (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) &&
5329 ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) ||
5330 (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0))
5331 ) {
5332 $result = Information\ExcelError::VALUE();
5333 } elseif (is_array($operand1) || is_array($operand2)) {
5334 // Ensure that both operands are arrays/matrices
5335 if (is_array($operand1)) {
5336 foreach ($operand1 as $key => $value) {
5337 $operand1[$key] = Functions::flattenArray($value);
5338 }
5339 }
5340 if (is_array($operand2)) {
5341 foreach ($operand2 as $key => $value) {
5342 $operand2[$key] = Functions::flattenArray($value);
5343 }
5344 }
5345 [$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 2);
5346
5347 for ($row = 0; $row < $rows; ++$row) {
5348 for ($column = 0; $column < $columns; ++$column) {
5349 if ($operand1[$row][$column] === null) {
5350 $operand1[$row][$column] = 0;
5351 } elseif (!self::isNumericOrBool($operand1[$row][$column])) {
5352 $operand1[$row][$column] = self::makeError($operand1[$row][$column]);
5353
5354 continue;
5355 }
5356 if ($operand2[$row][$column] === null) {
5357 $operand2[$row][$column] = 0;
5358 } elseif (!self::isNumericOrBool($operand2[$row][$column])) {
5359 $operand1[$row][$column] = self::makeError($operand2[$row][$column]);
5360
5361 continue;
5362 }
5363 switch ($operation) {
5364 case '+':
5365 $operand1[$row][$column] += $operand2[$row][$column];
5366
5367 break;
5368 case '-':
5369 $operand1[$row][$column] -= $operand2[$row][$column];
5370
5371 break;
5372 case '*':
5373 $operand1[$row][$column] *= $operand2[$row][$column];
5374
5375 break;
5376 case '/':
5377 if ($operand2[$row][$column] == 0) {
5378 $operand1[$row][$column] = Information\ExcelError::DIV0();
5379 } else {
5380 $operand1[$row][$column] /= $operand2[$row][$column];
5381 }
5382
5383 break;
5384 case '^':
5385 $operand1[$row][$column] = $operand1[$row][$column] ** $operand2[$row][$column];
5386
5387 break;
5388
5389 default:
5390 throw new Exception('Unsupported numeric binary operation');
5391 }
5392 }
5393 }
5394 $result = $operand1;
5395 } else {
5396 // If we're dealing with non-matrix operations, execute the necessary operation
5397 switch ($operation) {
5398 // Addition
5399 case '+':
5400 $result = $operand1 + $operand2;
5401
5402 break;
5403 // Subtraction
5404 case '-':
5405 $result = $operand1 - $operand2;
5406
5407 break;
5408 // Multiplication
5409 case '*':
5410 $result = $operand1 * $operand2;
5411
5412 break;
5413 // Division
5414 case '/':
5415 if ($operand2 == 0) {
5416 // Trap for Divide by Zero error
5417 $stack->push('Error', Information\ExcelError::DIV0());
5418 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails(Information\ExcelError::DIV0()));
5419
5420 return false;
5421 }
5422 $result = $operand1 / $operand2;
5423
5424 break;
5425 // Power
5426 case '^':
5427 $result = $operand1 ** $operand2;
5428
5429 break;
5430
5431 default:
5432 throw new Exception('Unsupported numeric binary operation');
5433 }
5434 }
5435
5436 // Log the result details
5437 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
5438 // And push the result onto the stack
5439 $stack->push('Value', $result);
5440
5441 return $result;
5442 }
5443
5444 /**
5445 * Trigger an error, but nicely, if need be.
5446 *
5447 * @return false
5448 */
5449 protected function raiseFormulaError(string $errorMessage, int $code = 0, ?Throwable $exception = null)
5450 {
5451 $this->formulaError = $errorMessage;
5452 $this->cyclicReferenceStack->clear();
5453 $suppress = /** @scrutinizer ignore-deprecated */ $this->suppressFormulaErrors ?? $this->suppressFormulaErrorsNew;
5454 if (!$suppress) {
5455 throw new Exception($errorMessage, $code, $exception);
5456 }
5457
5458 return false;
5459 }
5460
5461 /**
5462 * Extract range values.
5463 *
5464 * @param string $range String based range representation
5465 * @param Worksheet $worksheet Worksheet
5466 * @param bool $resetLog Flag indicating whether calculation log should be reset or not
5467 *
5468 * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
5469 */
5470 public function extractCellRange(&$range = 'A1', ?Worksheet $worksheet = null, $resetLog = true)
5471 {
5472 // Return value
5473 $returnValue = [];
5474
5475 if ($worksheet !== null) {
5476 $worksheetName = $worksheet->getTitle();
5477
5478 if (strpos($range, '!') !== false) {
5479 [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true);
5480 $worksheet = ($this->spreadsheet === null) ? null : $this->spreadsheet->getSheetByName($worksheetName);
5481 }
5482
5483 // Extract range
5484 $aReferences = Coordinate::extractAllCellReferencesInRange($range);
5485 $range = "'" . $worksheetName . "'" . '!' . $range;
5486 $currentCol = '';
5487 $currentRow = 0;
5488 if (!isset($aReferences[1])) {
5489 // Single cell in range
5490 sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
5491 if ($worksheet !== null && $worksheet->cellExists($aReferences[0])) {
5492 $returnValue[$currentRow][$currentCol] = $worksheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5493 } else {
5494 $returnValue[$currentRow][$currentCol] = null;
5495 }
5496 } else {
5497 // Extract cell data for all cells in the range
5498 foreach ($aReferences as $reference) {
5499 // Extract range
5500 sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
5501 if ($worksheet !== null && $worksheet->cellExists($reference)) {
5502 $returnValue[$currentRow][$currentCol] = $worksheet->getCell($reference)->getCalculatedValue($resetLog);
5503 } else {
5504 $returnValue[$currentRow][$currentCol] = null;
5505 }
5506 }
5507 }
5508 }
5509
5510 return $returnValue;
5511 }
5512
5513 /**
5514 * Extract range values.
5515 *
5516 * @param string $range String based range representation
5517 * @param null|Worksheet $worksheet Worksheet
5518 * @param bool $resetLog Flag indicating whether calculation log should be reset or not
5519 *
5520 * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned.
5521 */
5522 public function extractNamedRange(string &$range = 'A1', ?Worksheet $worksheet = null, $resetLog = true)
5523 {
5524 // Return value
5525 $returnValue = [];
5526
5527 if ($worksheet !== null) {
5528 if (strpos($range, '!') !== false) {
5529 [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true);
5530 $worksheet = ($this->spreadsheet === null) ? null : $this->spreadsheet->getSheetByName($worksheetName);
5531 }
5532
5533 // Named range?
5534 $namedRange = ($worksheet === null) ? null : DefinedName::resolveName($range, $worksheet);
5535 if ($namedRange === null) {
5536 return Information\ExcelError::REF();
5537 }
5538
5539 $worksheet = $namedRange->getWorksheet();
5540 $range = $namedRange->getValue();
5541 $splitRange = Coordinate::splitRange($range);
5542 // Convert row and column references
5543 if ($worksheet !== null && ctype_alpha($splitRange[0][0])) {
5544 $range = $splitRange[0][0] . '1:' . $splitRange[0][1] . $worksheet->getHighestRow();
5545 } elseif ($worksheet !== null && ctype_digit($splitRange[0][0])) {
5546 $range = 'A' . $splitRange[0][0] . ':' . $worksheet->getHighestColumn() . $splitRange[0][1];
5547 }
5548
5549 // Extract range
5550 $aReferences = Coordinate::extractAllCellReferencesInRange($range);
5551 if (!isset($aReferences[1])) {
5552 // Single cell (or single column or row) in range
5553 [$currentCol, $currentRow] = Coordinate::coordinateFromString($aReferences[0]);
5554 if ($worksheet !== null && $worksheet->cellExists($aReferences[0])) {
5555 $returnValue[$currentRow][$currentCol] = $worksheet->getCell($aReferences[0])->getCalculatedValue($resetLog);
5556 } else {
5557 $returnValue[$currentRow][$currentCol] = null;
5558 }
5559 } else {
5560 // Extract cell data for all cells in the range
5561 foreach ($aReferences as $reference) {
5562 // Extract range
5563 [$currentCol, $currentRow] = Coordinate::coordinateFromString($reference);
5564 if ($worksheet !== null && $worksheet->cellExists($reference)) {
5565 $returnValue[$currentRow][$currentCol] = $worksheet->getCell($reference)->getCalculatedValue($resetLog);
5566 } else {
5567 $returnValue[$currentRow][$currentCol] = null;
5568 }
5569 }
5570 }
5571 }
5572
5573 return $returnValue;
5574 }
5575
5576 /**
5577 * Is a specific function implemented?
5578 *
5579 * @param string $function Function Name
5580 *
5581 * @return bool
5582 */
5583 public function isImplemented($function)
5584 {
5585 $function = strtoupper($function);
5586 $notImplemented = !isset(self::$phpSpreadsheetFunctions[$function]) || (is_array(self::$phpSpreadsheetFunctions[$function]['functionCall']) && self::$phpSpreadsheetFunctions[$function]['functionCall'][1] === 'DUMMY');
5587
5588 return !$notImplemented;
5589 }
5590
5591 /**
5592 * Get a list of all implemented functions as an array of function objects.
5593 */
5594 public static function getFunctions(): array
5595 {
5596 return self::$phpSpreadsheetFunctions;
5597 }
5598
5599 /**
5600 * Get a list of implemented Excel function names.
5601 *
5602 * @return array
5603 */
5604 public function getImplementedFunctionNames()
5605 {
5606 $returnValue = [];
5607 foreach (self::$phpSpreadsheetFunctions as $functionName => $function) {
5608 if ($this->isImplemented($functionName)) {
5609 $returnValue[] = $functionName;
5610 }
5611 }
5612
5613 return $returnValue;
5614 }
5615
5616 private function addDefaultArgumentValues(array $functionCall, array $args, array $emptyArguments): array
5617 {
5618 $reflector = new ReflectionMethod($functionCall[0], $functionCall[1]);
5619 $methodArguments = $reflector->getParameters();
5620
5621 if (count($methodArguments) > 0) {
5622 // Apply any defaults for empty argument values
5623 foreach ($emptyArguments as $argumentId => $isArgumentEmpty) {
5624 if ($isArgumentEmpty === true) {
5625 $reflectedArgumentId = count($args) - (int) $argumentId - 1;
5626 if (
5627 !array_key_exists($reflectedArgumentId, $methodArguments) ||
5628 $methodArguments[$reflectedArgumentId]->isVariadic()
5629 ) {
5630 break;
5631 }
5632
5633 $args[$argumentId] = $this->getArgumentDefaultValue($methodArguments[$reflectedArgumentId]);
5634 }
5635 }
5636 }
5637
5638 return $args;
5639 }
5640
5641 /**
5642 * @return null|mixed
5643 */
5644 private function getArgumentDefaultValue(ReflectionParameter $methodArgument)
5645 {
5646 $defaultValue = null;
5647
5648 if ($methodArgument->isDefaultValueAvailable()) {
5649 $defaultValue = $methodArgument->getDefaultValue();
5650 if ($methodArgument->isDefaultValueConstant()) {
5651 $constantName = $methodArgument->getDefaultValueConstantName() ?? '';
5652 // read constant value
5653 if (strpos($constantName, '::') !== false) {
5654 [$className, $constantName] = explode('::', $constantName);
5655 $constantReflector = new ReflectionClassConstant($className, $constantName);
5656
5657 return $constantReflector->getValue();
5658 }
5659
5660 return constant($constantName);
5661 }
5662 }
5663
5664 return $defaultValue;
5665 }
5666
5667 /**
5668 * Add cell reference if needed while making sure that it is the last argument.
5669 *
5670 * @param bool $passCellReference
5671 * @param array|string $functionCall
5672 *
5673 * @return array
5674 */
5675 private function addCellReference(array $args, $passCellReference, $functionCall, ?Cell $cell = null)
5676 {
5677 if ($passCellReference) {
5678 if (is_array($functionCall)) {
5679 $className = $functionCall[0];
5680 $methodName = $functionCall[1];
5681
5682 $reflectionMethod = new ReflectionMethod($className, $methodName);
5683 $argumentCount = count($reflectionMethod->getParameters());
5684 while (count($args) < $argumentCount - 1) {
5685 $args[] = null;
5686 }
5687 }
5688
5689 $args[] = $cell;
5690 }
5691
5692 return $args;
5693 }
5694
5695 /**
5696 * @return mixed|string
5697 */
5698 private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksheet $cellWorksheet, Stack $stack)
5699 {
5700 $definedNameScope = $namedRange->getScope();
5701 if ($definedNameScope !== null && $definedNameScope !== $cellWorksheet) {
5702 // The defined name isn't in our current scope, so #REF
5703 $result = Information\ExcelError::REF();
5704 $stack->push('Error', $result, $namedRange->getName());
5705
5706 return $result;
5707 }
5708
5709 $definedNameValue = $namedRange->getValue();
5710 $definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
5711 $definedNameWorksheet = $namedRange->getWorksheet();
5712
5713 if ($definedNameValue[0] !== '=') {
5714 $definedNameValue = '=' . $definedNameValue;
5715 }
5716
5717 $this->debugLog->writeDebugLog('Defined Name is a %s with a value of %s', $definedNameType, $definedNameValue);
5718
5719 $recursiveCalculationCell = ($definedNameWorksheet !== null && $definedNameWorksheet !== $cellWorksheet)
5720 ? $definedNameWorksheet->getCell('A1')
5721 : $cell;
5722 $recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate();
5723
5724 // Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns
5725 $definedNameValue = ReferenceHelper::getInstance()
5726 ->updateFormulaReferencesAnyWorksheet(
5727 $definedNameValue,
5728 Coordinate::columnIndexFromString(
5729 $cell->getColumn()
5730 ) - 1,
5731 $cell->getRow() - 1
5732 );
5733
5734 $this->debugLog->writeDebugLog('Value adjusted for relative references is %s', $definedNameValue);
5735
5736 $recursiveCalculator = new self($this->spreadsheet);
5737 $recursiveCalculator->getDebugLog()->setWriteDebugLog($this->getDebugLog()->getWriteDebugLog());
5738 $recursiveCalculator->getDebugLog()->setEchoDebugLog($this->getDebugLog()->getEchoDebugLog());
5739 $result = $recursiveCalculator->_calculateFormulaValue($definedNameValue, $recursiveCalculationCellAddress, $recursiveCalculationCell, true);
5740
5741 if ($this->getDebugLog()->getWriteDebugLog()) {
5742 $this->debugLog->mergeDebugLog(array_slice($recursiveCalculator->getDebugLog()->getLog(), 3));
5743 $this->debugLog->writeDebugLog('Evaluation Result for Named %s %s is %s', $definedNameType, $namedRange->getName(), $this->showTypeDetails($result));
5744 }
5745
5746 $stack->push('Defined Name', $result, $namedRange->getName());
5747
5748 return $result;
5749 }
5750
5751 public function setSuppressFormulaErrors(bool $suppressFormulaErrors): void
5752 {
5753 $this->suppressFormulaErrorsNew = $suppressFormulaErrors;
5754 }
5755
5756 public function getSuppressFormulaErrors(): bool
5757 {
5758 return $this->suppressFormulaErrorsNew;
5759 }
5760
5761 /** @param mixed $arg */
5762 private static function doNothing($arg): bool
5763 {
5764 return (bool) $arg;
5765 }
5766
5767 /**
5768 * @param mixed $operand1
5769 *
5770 * @return mixed
5771 */
5772 private static function boolToString($operand1)
5773 {
5774 if (is_bool($operand1)) {
5775 $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE'];
5776 } elseif ($operand1 === null) {
5777 $operand1 = '';
5778 }
5779
5780 return $operand1;
5781 }
5782
5783 /** @param mixed $operand */
5784 private static function isNumericOrBool($operand): bool
5785 {
5786 return is_numeric($operand) || is_bool($operand);
5787 }
5788
5789 /** @param mixed $operand */
5790 private static function makeError($operand = ''): string
5791 {
5792 return Information\ErrorValue::isError($operand) ? $operand : Information\ExcelError::VALUE();
5793 }
5794 }
5795