Codex's updates...

This commit is contained in:
2026-10-18 19:27:17 -04:00
parent a1643eb1b7
commit 02223083f6
338 changed files with 14757 additions and 7986 deletions
@@ -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;
}
}