generated from jric11/baseProject
Codex's updates...
This commit is contained in:
+193
-48
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
use Composer\Pcre\Preg; // many pregs in this program use u modifier, which has side-effects which make it unsuitable for this
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\BranchPruner;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
|
||||
@@ -13,6 +14,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\DefinedName;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\NamedRange;
|
||||
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
@@ -36,7 +38,7 @@ class Calculation extends CalculationLocale
|
||||
const CALCULATION_REGEXP_OPENBRACE = '\(';
|
||||
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
|
||||
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?((?:__xludf\.)?[\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
|
||||
// Cell reference (cell or range of cells, with or without a sheet reference)
|
||||
// Cell reference, with or without a sheet reference)
|
||||
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
|
||||
// Used only to detect spill operator #
|
||||
const CALCULATION_REGEXP_CELLREF_SPILL = '/' . self::CALCULATION_REGEXP_CELLREF . '#/i';
|
||||
@@ -88,9 +90,22 @@ class Calculation extends CalculationLocale
|
||||
*/
|
||||
private bool $calculationCacheEnabled = true;
|
||||
|
||||
/**
|
||||
* Maximum number of entries in the formula token cache.
|
||||
* Default 0 (disabled). Set via setFormulaTokenCacheMaxSize() to enable.
|
||||
*/
|
||||
private int $formulaTokenCacheMaxSize = 0;
|
||||
|
||||
/**
|
||||
* Cache of parsed formula tokens, keyed by the raw formula string.
|
||||
*
|
||||
* @var array<string, array<mixed>|bool>
|
||||
*/
|
||||
private array $formulaTokenCache = [];
|
||||
|
||||
private BranchPruner $branchPruner;
|
||||
|
||||
private bool $branchPruningEnabled = true;
|
||||
protected bool $branchPruningEnabled = true;
|
||||
|
||||
/**
|
||||
* List of operators that can be used within formulae
|
||||
@@ -239,6 +254,7 @@ class Calculation extends CalculationLocale
|
||||
{
|
||||
$this->clearCalculationCache();
|
||||
$this->branchPruner->clearBranchStore();
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,6 +380,44 @@ class Calculation extends CalculationLocale
|
||||
$this->calculationCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the formula token cache.
|
||||
*/
|
||||
public function clearFormulaTokenCache(): void
|
||||
{
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current number of entries in the formula token cache.
|
||||
*/
|
||||
public function getFormulaTokenCacheSize(): int
|
||||
{
|
||||
return count($this->formulaTokenCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of entries in the formula token cache.
|
||||
* Set to 0 to disable caching (default), or a positive integer to enable.
|
||||
*/
|
||||
public function setFormulaTokenCacheMaxSize(int $size): self
|
||||
{
|
||||
$this->formulaTokenCacheMaxSize = max(0, $size);
|
||||
if ($this->formulaTokenCacheMaxSize === 0) {
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum number of entries allowed in the formula token cache.
|
||||
*/
|
||||
public function getFormulaTokenCacheMaxSize(): int
|
||||
{
|
||||
return $this->formulaTokenCacheMaxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear calculation cache for a specified worksheet.
|
||||
*/
|
||||
@@ -415,7 +469,7 @@ class Calculation extends CalculationLocale
|
||||
{
|
||||
if (is_string($value)) {
|
||||
// Error values cannot be "wrapped"
|
||||
if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
|
||||
if (Preg::isMatch('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
|
||||
// Return Excel errors "as is"
|
||||
return $value;
|
||||
}
|
||||
@@ -494,13 +548,13 @@ class Calculation extends CalculationLocale
|
||||
try {
|
||||
$value = $cell->getValue();
|
||||
if (is_string($value) && $cell->getDataType() === DataType::TYPE_FORMULA) {
|
||||
$value = preg_replace_callback(
|
||||
$value = Preg::replaceCallback(
|
||||
self::CALCULATION_REGEXP_CELLREF_SPILL,
|
||||
fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
|
||||
$value
|
||||
);
|
||||
}
|
||||
$result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore-line
|
||||
$result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore argument.type ($value can be mixed not string)
|
||||
if ($this->spreadsheet === null) {
|
||||
throw new Exception('null spreadsheet in calculateCellValue');
|
||||
}
|
||||
@@ -520,7 +574,7 @@ class Calculation extends CalculationLocale
|
||||
$cellAddress = array_pop($this->cellStack);
|
||||
}
|
||||
if ($this->spreadsheet !== null && is_array($cellAddress) && array_key_exists('sheet', $cellAddress)) {
|
||||
$sheetName = $cellAddress['sheet'] ?? null;
|
||||
$sheetName = $cellAddress['sheet'];
|
||||
$testSheet = is_string($sheetName) ? $this->spreadsheet->getSheetByName($sheetName) : null;
|
||||
if ($testSheet !== null && array_key_exists('cell', $cellAddress)) {
|
||||
/** @var array{cell: string} $cellAddress */
|
||||
@@ -557,11 +611,17 @@ class Calculation extends CalculationLocale
|
||||
*/
|
||||
public function parseFormula(string $formula): array|bool
|
||||
{
|
||||
$formula = preg_replace_callback(
|
||||
// Check the formula token cache first (only when caching is enabled)
|
||||
if ($this->formulaTokenCacheMaxSize > 0 && isset($this->formulaTokenCache[$formula])) {
|
||||
return $this->formulaTokenCache[$formula];
|
||||
}
|
||||
|
||||
$originalFormula = $formula;
|
||||
$formula = Preg::replaceCallback(
|
||||
self::CALCULATION_REGEXP_CELLREF_SPILL,
|
||||
fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
|
||||
$formula
|
||||
) ?? $formula;
|
||||
);
|
||||
// Basic validation that this is indeed a formula
|
||||
// We return an empty array if not
|
||||
$formula = trim($formula);
|
||||
@@ -574,7 +634,21 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
|
||||
// Parse the formula and return the token stack
|
||||
return $this->internalParseFormula($formula);
|
||||
$result = $this->internalParseFormula($formula);
|
||||
|
||||
// Cache the result when caching is enabled (clear cache if it exceeds the maximum size)
|
||||
if ($this->formulaTokenCacheMaxSize > 0) {
|
||||
// Phpstan says if condition is always false,
|
||||
// but coverage report says next statement is covered.
|
||||
if (count($this->formulaTokenCache) >= $this->formulaTokenCacheMaxSize) {
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
// Cache key is the original formula string (before ANCHORARRAY transformation)
|
||||
// to ensure consistent lookup regardless of internal transformations.
|
||||
$this->formulaTokenCache[$originalFormula] = $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,8 +731,9 @@ class Calculation extends CalculationLocale
|
||||
return self::wrapResult((string) $formula);
|
||||
}
|
||||
|
||||
// https://www.reddit.com/r/excel/comments/chr41y/cmd_formula_stopped_working_since_last_update/
|
||||
if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) {
|
||||
return self::wrapResult($formula);
|
||||
return ExcelError::REF(); // returns #BLOCKED in newer versions
|
||||
}
|
||||
|
||||
// Basic validation that this is indeed a formula
|
||||
@@ -914,13 +989,14 @@ class Calculation extends CalculationLocale
|
||||
$pad = $rpad = ', ';
|
||||
foreach ($value as $row) {
|
||||
if (is_array($row)) {
|
||||
$returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
|
||||
$returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row)); // @phpstan-ignore argument.type (array_map can theoretically return array<mixed> not array<string>)
|
||||
$rpad = '; ';
|
||||
} else {
|
||||
$returnMatrix[] = $this->showValue($row);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var string[] $returnMatrix */
|
||||
return '{ ' . implode($rpad, $returnMatrix) . ' }';
|
||||
} elseif (is_string($value) && (trim($value, self::FORMULA_STRING_QUOTE) == $value)) {
|
||||
return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
|
||||
@@ -1052,6 +1128,50 @@ class Calculation extends CalculationLocale
|
||||
'>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison
|
||||
];
|
||||
|
||||
/** @param array<?string> $matches */
|
||||
private function unionForComma(array $matches): string
|
||||
{
|
||||
$matches5 = (string) $matches[5];
|
||||
// Weirdly, the regexp which get us here for issue 4832
|
||||
// only gets us here for Php8.4+. 8.4 introduced
|
||||
// major changes for PCRE, but I cannot identify
|
||||
// the exact change which caused this discrepancy.
|
||||
// I do plan to update coverage to 8.4 at some point,
|
||||
// and I can remove the coverage annotations after that.
|
||||
// @codeCoverageIgnoreStart
|
||||
if (str_contains($matches5, '(') && !str_contains($matches5, ')')) {
|
||||
if ($this->spreadsheet !== null) {
|
||||
if ($this->spreadsheet->getSheetByName($matches5) === null) {
|
||||
$matches0 = (string) $matches[0];
|
||||
$this->debugLog->writeDebugLog('Not Reformulating %s', $matches0);
|
||||
|
||||
return $matches0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
$matches1 = (string) $matches[1];
|
||||
$matches2 = (string) $matches[2];
|
||||
|
||||
return $matches1 . str_replace(',', '∪', $matches2);
|
||||
}
|
||||
|
||||
private const CELL_OR_CELLRANGE_OR_DEFINED_NAME
|
||||
= '(?:'
|
||||
. self::CALCULATION_REGEXP_CELLREF // cell address
|
||||
. '(?::' . self::CALCULATION_REGEXP_CELLREF . ')?' // optional range address, non-capturing
|
||||
. '|' . self::CALCULATION_REGEXP_DEFINEDNAME
|
||||
. ')'
|
||||
;
|
||||
|
||||
public const UNIONABLE_COMMAS = '/((?:[,(]|^)\s*)' // comma or open paren or start of string, followed by optional whitespace
|
||||
. '([(]' // open paren
|
||||
. self::CELL_OR_CELLRANGE_OR_DEFINED_NAME // cell address
|
||||
. '(?:\s*,\s*' // optioonal whitespace, comma, optional whitespace, non-capturing
|
||||
. self::CELL_OR_CELLRANGE_OR_DEFINED_NAME // cell address
|
||||
. ')+' // one or more occurrences
|
||||
. '\s*[)])/i'; // optional whitespace, end paren
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>|false
|
||||
*/
|
||||
@@ -1060,6 +1180,12 @@ class Calculation extends CalculationLocale
|
||||
if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldFormula = $formula;
|
||||
$formula = Preg::replaceCallback(self::UNIONABLE_COMMAS, $this->unionForComma(...), $formula);
|
||||
if ($oldFormula !== $formula) {
|
||||
$this->debugLog->writeDebugLog('Reformulated as %s', $formula);
|
||||
}
|
||||
$phpSpreadsheetFunctions = &self::getFunctionsAddress();
|
||||
|
||||
// If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
|
||||
@@ -1179,7 +1305,7 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
}
|
||||
} elseif (is_string($expectedArgumentCount) && $expectedArgumentCount !== '*') {
|
||||
if (1 !== preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) {
|
||||
if (!Preg::isMatch('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) {
|
||||
$argMatch = ['', '', '', ''];
|
||||
}
|
||||
switch ($argMatch[2]) {
|
||||
@@ -1236,16 +1362,14 @@ class Calculation extends CalculationLocale
|
||||
// because at least the braces are paired up (at this stage in the formula)
|
||||
// MS Excel allows this if the content is cell references; but doesn't allow actual values,
|
||||
// but at this point, we can't differentiate (so allow both)
|
||||
return $this->raiseFormulaError('Formula Error: Unexpected ,');
|
||||
/* The following code may be a better choice, but, with
|
||||
the other changes for this PR, I can no longer come up
|
||||
with a test case that gets here
|
||||
//return $this->raiseFormulaError('Formula Error: Unexpected ,');
|
||||
|
||||
$stack->push('Binary Operator', '∪');
|
||||
|
||||
++$index;
|
||||
$expectingOperator = false;
|
||||
|
||||
continue;*/
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var array<string, int> $d */
|
||||
@@ -1267,11 +1391,12 @@ class Calculation extends CalculationLocale
|
||||
// do we now have a function/variable/number?
|
||||
$expectingOperator = true;
|
||||
$expectingOperand = false;
|
||||
$val = $match[1] ?? ''; //* @phpstan-ignore-line
|
||||
$val = $match[1] ?? '';
|
||||
$length = strlen($val);
|
||||
|
||||
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
|
||||
$val = (string) preg_replace('/\s/u', '', $val);
|
||||
// $val is known to be valid unicode from statement above, so Preg::replace is okay even with u modifier
|
||||
$val = Preg::replace('/\s/u', '', $val);
|
||||
if (isset($phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
|
||||
$valToUpper = strtoupper($val);
|
||||
} else {
|
||||
@@ -1410,7 +1535,7 @@ class Calculation extends CalculationLocale
|
||||
$refSheet = $pCellParent->getParentOrThrow()->getSheetByName($rangeSheetRef);
|
||||
}
|
||||
|
||||
if (ctype_digit($val) && $val <= 1048576) {
|
||||
if (ctype_digit($val) && $val <= AddressRange::MAX_ROW) {
|
||||
// Row range
|
||||
$stackItemType = 'Row Reference';
|
||||
$valx = $val;
|
||||
@@ -1419,7 +1544,7 @@ class Calculation extends CalculationLocale
|
||||
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
|
||||
// Column range
|
||||
$stackItemType = 'Column Reference';
|
||||
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
|
||||
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow() : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
|
||||
$val = "{$rangeWS2}{$val}{$endRowColRef}";
|
||||
}
|
||||
$stackItemReference = $val;
|
||||
@@ -1445,7 +1570,7 @@ class Calculation extends CalculationLocale
|
||||
// unescape any apostrophes or double quotes in worksheet name
|
||||
$val = str_replace(["''", '""'], ["'", '"'], $val);
|
||||
$column = 'A';
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevop must be non-null), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
$column = $pCellParent->getHighestDataColumn($val);
|
||||
}
|
||||
$val = "{$rowRangeReference[2]}{$column}{$rowRangeReference[7]}";
|
||||
@@ -1459,7 +1584,7 @@ class Calculation extends CalculationLocale
|
||||
// unescape any apostrophes or double quotes in worksheet name
|
||||
$val = str_replace(["''", '""'], ["'", '"'], $val);
|
||||
$row = '1';
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevOp must be non-null?), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
$row = $pCellParent->getHighestDataRow($val);
|
||||
}
|
||||
$val = "{$val}{$row}";
|
||||
@@ -1610,7 +1735,7 @@ class Calculation extends CalculationLocale
|
||||
foreach ($tokens as $tokenIdx => $tokenData) {
|
||||
/** @var mixed[] $tokenData */
|
||||
$this->processingAnchorArray = false;
|
||||
if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore-line
|
||||
if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore offsetAccess.nonOffsetAccessible ($tokens might be mixed not array)
|
||||
$this->processingAnchorArray = true;
|
||||
}
|
||||
$token = $tokenData['value'];
|
||||
@@ -1712,7 +1837,7 @@ class Calculation extends CalculationLocale
|
||||
return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
} elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore-line
|
||||
} elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore offsetAccess.invalidOffset ($token is mixed)
|
||||
// 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
|
||||
// We must have two operands, error if we don't
|
||||
$operand2Data = $stack->pop();
|
||||
@@ -1830,7 +1955,7 @@ class Calculation extends CalculationLocale
|
||||
if ($breakNeeded) {
|
||||
break;
|
||||
}
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
|
||||
if ($pCellParent !== null && $this->spreadsheet !== null) {
|
||||
$cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
|
||||
} else {
|
||||
@@ -1901,7 +2026,7 @@ class Calculation extends CalculationLocale
|
||||
} elseif (Information\ErrorValue::isError($operand2)) {
|
||||
$result = $operand2;
|
||||
} else {
|
||||
$result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore-line
|
||||
$result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore binaryOp.invalid (unwrapresult can return mixed rather than string)
|
||||
$result = StringHelper::substring(
|
||||
$result,
|
||||
0,
|
||||
@@ -1934,8 +2059,8 @@ class Calculation extends CalculationLocale
|
||||
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
|
||||
$stack->push('Error', ExcelError::null(), null);
|
||||
} else {
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore-line
|
||||
. Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
|
||||
. Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
|
||||
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
|
||||
$stack->push('Value', $cellIntersect, $cellRef);
|
||||
}
|
||||
@@ -1950,13 +2075,13 @@ class Calculation extends CalculationLocale
|
||||
|
||||
break;
|
||||
}
|
||||
} elseif (($token === '~') || ($token === '%')) {
|
||||
} elseif (($token === '~') || ($token === '%')) { // @phpstan-ignore booleanOr.alwaysFalse (phpstan says token can't be plain string), identical.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
|
||||
if (($arg = $stack->pop()) === null) {
|
||||
return $this->raiseFormulaError('Internal error - Operand value missing from stack');
|
||||
}
|
||||
$arg = $arg['value'];
|
||||
if ($token === '~') {
|
||||
if ($token === '~') { // @phpstan-ignore identical.alwaysFalse (phpstan says token can't be plain string)
|
||||
$this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg));
|
||||
$multiplier = -1;
|
||||
} else {
|
||||
@@ -1988,20 +2113,27 @@ class Calculation extends CalculationLocale
|
||||
} else {
|
||||
$this->executeNumericBinaryOperation($multiplier, $arg, '*', $stack);
|
||||
}
|
||||
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) {
|
||||
} elseif (Preg::isMatch('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) {
|
||||
$cellRef = null;
|
||||
|
||||
/* Phpstan says matches[8/9/10] is never set,
|
||||
and code coverage report seems to confirm.
|
||||
Appease PhpStan for now;
|
||||
probably delete this block later.
|
||||
regex101.com confirms - only 7 capturing groups.
|
||||
My theory is that this code expected regexp to
|
||||
match cell *or* cellRange, but it does not
|
||||
match the latter. Retain the code for now in case
|
||||
we do want to add the range match later.
|
||||
Probably delete this block later.
|
||||
Until delete happens, turn code coverage off.
|
||||
*/
|
||||
if (isset($matches[self::$matchIndex8])) {
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($cell === null) {
|
||||
// We can't access the range, so return a REF error
|
||||
$cellValue = ExcelError::REF();
|
||||
} else {
|
||||
$cellRef = $matches[6] . $matches[7] . ':' . $matches[self::$matchIndex9] . $matches[self::$matchIndex10];
|
||||
$matches[2] = (string) $matches[2];
|
||||
if ($matches[2] > '') {
|
||||
$matches[2] = trim($matches[2], "\"'");
|
||||
if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
|
||||
@@ -2026,12 +2158,14 @@ class Calculation extends CalculationLocale
|
||||
$this->debugLog->writeDebugLog('Evaluation Result for cells %s is %s', $cellRef, $this->showTypeDetails($cellValue));
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
} else {
|
||||
if ($cell === null) {
|
||||
// We can't access the cell, so return a REF error
|
||||
$cellValue = ExcelError::REF();
|
||||
} else {
|
||||
$cellRef = $matches[6] . $matches[7];
|
||||
$matches[2] = (string) $matches[2];
|
||||
if ($matches[2] > '') {
|
||||
$matches[2] = trim($matches[2], "\"'");
|
||||
if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
|
||||
@@ -2042,7 +2176,11 @@ class Calculation extends CalculationLocale
|
||||
if ($pCellParent !== null && $this->spreadsheet !== null) {
|
||||
$cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
|
||||
if ($cellSheet && !$cellSheet->cellExists($cellRef)) {
|
||||
$cellSheet->setCellValue($cellRef, null);
|
||||
try {
|
||||
$cellSheet->setCellValue($cellRef, null);
|
||||
} catch (SpreadsheetException) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
if ($cellSheet && $cellSheet->cellExists($cellRef)) {
|
||||
$cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
|
||||
@@ -2073,7 +2211,7 @@ class Calculation extends CalculationLocale
|
||||
$cellValue = array_shift($cellValue);
|
||||
}
|
||||
if (is_string($cellValue)) {
|
||||
$cellValue = preg_replace('/"/', '""', $cellValue);
|
||||
$cellValue = Preg::replace('/"/', '""', $cellValue);
|
||||
}
|
||||
$this->debugLog->writeDebugLog('Scalar Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue));
|
||||
}
|
||||
@@ -2117,7 +2255,7 @@ class Calculation extends CalculationLocale
|
||||
$a = $argCount - $i - 1;
|
||||
if (
|
||||
($passByReference)
|
||||
&& (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore-line
|
||||
&& (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore offsetAccess.nonOffsetAccessible (possibly need to pass 2 arguments to isset)
|
||||
&& ($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
|
||||
) {
|
||||
/** @var mixed[] $arg */
|
||||
@@ -2184,7 +2322,7 @@ class Calculation extends CalculationLocale
|
||||
if ($functionName !== 'MKMATRIX') {
|
||||
if ($this->debugLog->getWriteDebugLog()) {
|
||||
krsort($argArrayVals);
|
||||
$this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)));
|
||||
$this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals))); // @phpstan-ignore argument.type (flattenArray returns array<mixed> rather than array<string>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2221,7 +2359,7 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
} else {
|
||||
// if the token is a number, boolean, string or an Excel error, push it onto the stack
|
||||
/** @var ?string $token */
|
||||
/** @var null|numeric-string $token */
|
||||
if (isset(self::EXCEL_CONSTANTS[strtoupper($token ?? '')])) {
|
||||
$excelConstant = strtoupper("$token");
|
||||
$stack->push('Constant Value', self::EXCEL_CONSTANTS[$excelConstant]);
|
||||
@@ -2229,7 +2367,7 @@ class Calculation extends CalculationLocale
|
||||
$branchStore[$storeKey] = self::EXCEL_CONSTANTS[$excelConstant];
|
||||
}
|
||||
$this->debugLog->writeDebugLog('Evaluating Constant %s as %s', $excelConstant, $this->showTypeDetails(self::EXCEL_CONSTANTS[$excelConstant]));
|
||||
} elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore-line
|
||||
} elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore function.alreadyNarrowedType (phpstan says is_bool has argument of *NEVER*?), equal.alwaysFalse (ditto)
|
||||
/** @var array{type: string, reference: ?string} $tokenData */
|
||||
$stack->push($tokenData['type'], $token, $tokenData['reference']);
|
||||
if (isset($storeKey)) {
|
||||
@@ -2355,7 +2493,7 @@ class Calculation extends CalculationLocale
|
||||
$r = $stack->pop();
|
||||
$result[$x] = $r['value'];
|
||||
}
|
||||
} elseif (is_array($operand2) && is_array($operand1)) {
|
||||
} elseif (is_array($operand2) /*&& is_array($operand1)*/) {
|
||||
// Operand 1 and Operand 2 are both arrays
|
||||
if (!$recursingArrays) {
|
||||
self::checkMatrixOperands($operand1, $operand2, 2);
|
||||
@@ -2368,7 +2506,7 @@ class Calculation extends CalculationLocale
|
||||
$result[$x] = $r['value'];
|
||||
}
|
||||
} else {
|
||||
throw new Exception('Neither operand is an arra');
|
||||
throw new Exception('Neither operand is an array');
|
||||
}
|
||||
// Log the result details
|
||||
$this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result));
|
||||
@@ -2378,8 +2516,8 @@ class Calculation extends CalculationLocale
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return bool|mixed[] */
|
||||
private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool
|
||||
/** @return array<mixed>|bool|string */
|
||||
private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool|string
|
||||
{
|
||||
// If we're dealing with matrix operations, we want a matrix result
|
||||
if ((is_array($operand1)) || (is_array($operand2))) {
|
||||
@@ -2581,6 +2719,8 @@ class Calculation extends CalculationLocale
|
||||
if (!isset($aReferences[1])) {
|
||||
// Single cell in range
|
||||
sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
|
||||
/** @var string $currentCol */
|
||||
/** @var int $currentRow */
|
||||
if ($createCell && $worksheet !== null && !$worksheet->cellExists($aReferences[0])) {
|
||||
$worksheet->setCellValue($aReferences[0], null);
|
||||
}
|
||||
@@ -2600,6 +2740,8 @@ class Calculation extends CalculationLocale
|
||||
foreach ($aReferences as $reference) {
|
||||
// Extract range
|
||||
sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
|
||||
/** @var string $currentCol */
|
||||
/** @var int $currentRow */
|
||||
if ($createCell && $worksheet !== null && !$worksheet->cellExists($reference)) {
|
||||
$worksheet->setCellValue($reference, null);
|
||||
}
|
||||
@@ -2760,6 +2902,7 @@ class Calculation extends CalculationLocale
|
||||
// read constant value
|
||||
if (str_contains($constantName, '::')) {
|
||||
[$className, $constantName] = explode('::', $constantName);
|
||||
/** @var class-string $className */
|
||||
$constantReflector = new ReflectionClassConstant($className, $constantName);
|
||||
|
||||
return $constantReflector->getValue();
|
||||
@@ -2814,10 +2957,12 @@ class Calculation extends CalculationLocale
|
||||
$definedNameValue = $namedRange->getValue();
|
||||
$definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
|
||||
if ($definedNameType === 'Range') {
|
||||
if (preg_match('/^(.*!)?(.*)$/', $definedNameValue, $matches) === 1) {
|
||||
$matches2 = trim($matches[2]);
|
||||
$matches2 = preg_replace('/ +/', ' ∩ ', $matches2) ?? $matches2;
|
||||
$matches2 = preg_replace('/,/', ' ∪ ', $matches2) ?? $matches2;
|
||||
if (Preg::isMatch('/^(.*!)?(.*)$/', $definedNameValue, $matches)) {
|
||||
$matches2 = Preg::replace(
|
||||
['/ +/', '/,/'],
|
||||
[' ∩ ', ' ∪ '],
|
||||
trim($matches[2])
|
||||
);
|
||||
$definedNameValue = $matches[1] . $matches2;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user