Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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