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