Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

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