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
This commit is contained in:
Mark Baker 2021-06-10 08:49:53 +02:00 committed by GitHub
parent a340240a3f
commit a911e9bb7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 72 additions and 9 deletions

View File

@ -12,7 +12,9 @@ use PhpOffice\PhpSpreadsheet\ReferenceHelper;
use PhpOffice\PhpSpreadsheet\Shared; use PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use ReflectionClassConstant;
use ReflectionMethod; use ReflectionMethod;
use ReflectionParameter;
class Calculation class Calculation
{ {
@ -4108,7 +4110,7 @@ class Calculation
// If we've a comma when we're expecting an operand, then what we actually have is a null operand; // 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 // so push a null onto the stack
if (($expectingOperand) || (!$expectingOperator)) { 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 // make sure there was a function
$d = $stack->last(2); $d = $stack->last(2);
@ -4293,7 +4295,7 @@ class Calculation
++$index; ++$index;
} elseif ($opCharacter == ')') { // miscellaneous error checking } elseif ($opCharacter == ')') { // miscellaneous error checking
if ($expectingOperand) { if ($expectingOperand) {
$output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null]; $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => null];
$expectingOperand = false; $expectingOperand = false;
$expectingOperator = true; $expectingOperator = true;
} else { } else {
@ -4773,7 +4775,7 @@ class Calculation
$functionName = $matches[1]; $functionName = $matches[1];
$argCount = $stack->pop(); $argCount = $stack->pop();
$argCount = $argCount['value']; $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')); $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 if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) { // function
@ -4789,8 +4791,10 @@ class Calculation
$passByReference = isset(self::$controlFunctions[$functionName]['passByReference']); $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']);
$passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']); $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']);
} }
// get the arguments for this function // get the arguments for this function
$args = $argArrayVals = []; $args = $argArrayVals = [];
$emptyArguments = [];
for ($i = 0; $i < $argCount; ++$i) { for ($i = 0; $i < $argCount; ++$i) {
$arg = $stack->pop(); $arg = $stack->pop();
$a = $argCount - $i - 1; $a = $argCount - $i - 1;
@ -4801,18 +4805,19 @@ class Calculation
) { ) {
if ($arg['reference'] === null) { if ($arg['reference'] === null) {
$args[] = $cellID; $args[] = $cellID;
if ($functionName != 'MKMATRIX') { if ($functionName !== 'MKMATRIX') {
$argArrayVals[] = $this->showValue($cellID); $argArrayVals[] = $this->showValue($cellID);
} }
} else { } else {
$args[] = $arg['reference']; $args[] = $arg['reference'];
if ($functionName != 'MKMATRIX') { if ($functionName !== 'MKMATRIX') {
$argArrayVals[] = $this->showValue($arg['reference']); $argArrayVals[] = $this->showValue($arg['reference']);
} }
} }
} else { } else {
$emptyArguments[] = ($arg['type'] === 'Empty Argument');
$args[] = self::unwrapResult($arg['value']); $args[] = self::unwrapResult($arg['value']);
if ($functionName != 'MKMATRIX') { if ($functionName !== 'MKMATRIX') {
$argArrayVals[] = $this->showValue($arg['value']); $argArrayVals[] = $this->showValue($arg['value']);
} }
} }
@ -4820,13 +4825,18 @@ class Calculation
// Reverse the order of the arguments // Reverse the order of the arguments
krsort($args); krsort($args);
krsort($emptyArguments);
if ($argCount > 0) {
$args = $this->addDefaultArgumentValues($functionCall, $args, $emptyArguments);
}
if (($passByReference) && ($argCount == 0)) { if (($passByReference) && ($argCount == 0)) {
$args[] = $cellID; $args[] = $cellID;
$argArrayVals[] = $this->showValue($cellID); $argArrayVals[] = $this->showValue($cellID);
} }
if ($functionName != 'MKMATRIX') { if ($functionName !== 'MKMATRIX') {
if ($this->debugLog->getWriteDebugLog()) { if ($this->debugLog->getWriteDebugLog()) {
krsort($argArrayVals); krsort($argArrayVals);
$this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )'); $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )');
@ -4845,7 +4855,7 @@ class Calculation
$result = call_user_func_array($functionCall, $args); $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)); $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
} }
$stack->push('Value', self::wrapResult($result)); $stack->push('Value', self::wrapResult($result));
@ -4863,7 +4873,7 @@ class Calculation
} }
$this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant])); $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] == '#')) { } 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)) { if (isset($storeKey)) {
$branchStore[$storeKey] = $token; $branchStore[$storeKey] = $token;
} }
@ -5386,6 +5396,57 @@ class Calculation
return $returnValue; 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. * Add cell reference if needed while making sure that it is the last argument.
* *

View File

@ -77,4 +77,6 @@ return [
['exception', ''], ['exception', ''],
[48, 'B1'], [48, 'B1'],
[0, 'Q15'], [0, 'Q15'],
[52, '"21-Dec-2000", '],
[52, '"21-Dec-2000", null'],
]; ];