From a911e9bb7bf6c83d91bab1035246ab56fd1a310b Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Thu, 10 Jun 2021 08:49:53 +0200 Subject: [PATCH] Calculation engine empty arguments (#2143) * Initia work on differentiating between empty arguments and null arguments passed to Excel functions Previously we always passed a null value for an empty argument (i.e. where there was an argument separator in the function call without an argument.... PHP doesn't support empty arguments, so we needed to provide some value but then it wasn't possible to differentiate between a genuine null argument (either a literal null, or a null cell value) and the null that we were passing to represent an empty argument value. This change evaluates empty arguments within the calculation engine, and instead of passing a null, it reads the signature of the required Excel function, and passes the default value for that argument; so now a null argument really does mean a null value argument. * If the Excel function implementation doesn't accept any arguments; or once we reach a variadic argument, or try to pass more arguments than the method supports in its signature, then there's no point in checking for defaults, and to do so will lead to PHP errors, so break out of the default replacement loop --- .../Calculation/Calculation.php | 79 ++++++++++++++++--- tests/data/Calculation/DateTime/WEEKNUM.php | 2 + 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index d03b36236f..eb11143763 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -12,7 +12,9 @@ use PhpOffice\PhpSpreadsheet\Shared; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use ReflectionClassConstant; use ReflectionMethod; +use ReflectionParameter; class Calculation { @@ -4108,7 +4110,7 @@ private function internalParseFormula($formula, ?Cell $pCell = null) // If we've a comma when we're expecting an operand, then what we actually have is a null operand; // so push a null onto the stack if (($expectingOperand) || (!$expectingOperator)) { - $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null]; + $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => null]; } // make sure there was a function $d = $stack->last(2); @@ -4293,7 +4295,7 @@ private function internalParseFormula($formula, ?Cell $pCell = null) ++$index; } elseif ($opCharacter == ')') { // miscellaneous error checking if ($expectingOperand) { - $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null]; + $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => null]; $expectingOperand = false; $expectingOperator = true; } else { @@ -4773,7 +4775,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null) $functionName = $matches[1]; $argCount = $stack->pop(); $argCount = $argCount['value']; - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's')); } if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) { // function @@ -4789,8 +4791,10 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null) $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']); $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']); } + // get the arguments for this function $args = $argArrayVals = []; + $emptyArguments = []; for ($i = 0; $i < $argCount; ++$i) { $arg = $stack->pop(); $a = $argCount - $i - 1; @@ -4801,18 +4805,19 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null) ) { if ($arg['reference'] === null) { $args[] = $cellID; - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $argArrayVals[] = $this->showValue($cellID); } } else { $args[] = $arg['reference']; - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $argArrayVals[] = $this->showValue($arg['reference']); } } } else { + $emptyArguments[] = ($arg['type'] === 'Empty Argument'); $args[] = self::unwrapResult($arg['value']); - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $argArrayVals[] = $this->showValue($arg['value']); } } @@ -4820,13 +4825,18 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null) // Reverse the order of the arguments krsort($args); + krsort($emptyArguments); + + if ($argCount > 0) { + $args = $this->addDefaultArgumentValues($functionCall, $args, $emptyArguments); + } if (($passByReference) && ($argCount == 0)) { $args[] = $cellID; $argArrayVals[] = $this->showValue($cellID); } - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { if ($this->debugLog->getWriteDebugLog()) { krsort($argArrayVals); $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )'); @@ -4845,7 +4855,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null) $result = call_user_func_array($functionCall, $args); - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result)); } $stack->push('Value', self::wrapResult($result)); @@ -4863,7 +4873,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null) } $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant])); } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { - $stack->push('Value', $token); + $stack->push($tokenData['type'], $token, $tokenData['reference']); if (isset($storeKey)) { $branchStore[$storeKey] = $token; } @@ -5386,6 +5396,57 @@ public function getImplementedFunctionNames() return $returnValue; } + private function addDefaultArgumentValues(array $functionCall, array $args, array $emptyArguments): array + { + $reflector = new ReflectionMethod(implode('::', $functionCall)); + $methodArguments = $reflector->getParameters(); + + if (count($methodArguments) > 0) { + // Apply any defaults for empty argument values + foreach ($emptyArguments as $argumentId => $isArgumentEmpty) { + if ($isArgumentEmpty === true) { + $reflectedArgumentId = count($args) - $argumentId - 1; + if ( + !array_key_exists($reflectedArgumentId, $methodArguments) || + $methodArguments[$reflectedArgumentId]->isVariadic() + ) { + break; + } + + $args[$argumentId] = $this->getArgumentDefaultValue($methodArguments[$reflectedArgumentId]); + } + } + } + + return $args; + } + + /** + * @return null|mixed + */ + private function getArgumentDefaultValue(ReflectionParameter $methodArgument) + { + $defaultValue = null; + + if ($methodArgument->isDefaultValueAvailable()) { + $defaultValue = $methodArgument->getDefaultValue(); + if ($methodArgument->isDefaultValueConstant()) { + $constantName = $methodArgument->getDefaultValueConstantName() ?? ''; + // read constant value + if (strpos($constantName, '::') !== false) { + [$className, $constantName] = explode('::', $constantName); + $constantReflector = new ReflectionClassConstant($className, $constantName); + + return $constantReflector->getValue(); + } + + return constant($constantName); + } + } + + return $defaultValue; + } + /** * Add cell reference if needed while making sure that it is the last argument. * diff --git a/tests/data/Calculation/DateTime/WEEKNUM.php b/tests/data/Calculation/DateTime/WEEKNUM.php index 08de57385d..065da89d1c 100644 --- a/tests/data/Calculation/DateTime/WEEKNUM.php +++ b/tests/data/Calculation/DateTime/WEEKNUM.php @@ -77,4 +77,6 @@ ['exception', ''], [48, 'B1'], [0, 'Q15'], + [52, '"21-Dec-2000", '], + [52, '"21-Dec-2000", null'], ];