diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 72105814a3..062d2cfe09 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -35,7 +35,7 @@ class Calculation extends CalculationLocale // Opening bracket 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\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\('; + 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) const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; // Used only to detect spill operator # @@ -1727,6 +1727,16 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null, break; // Binary Operators case ':': // Range + if ($operand1Data['type'] === 'Error') { + $stack->push($operand1Data['type'], $operand1Data['value'], null); + + break; + } + if ($operand2Data['type'] === 'Error') { + $stack->push($operand2Data['type'], $operand2Data['value'], null); + + break; + } if ($operand1Data['type'] === 'Defined Name') { /** @var array{reference: string} $operand1Data */ if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false && $this->spreadsheet !== null) { @@ -2087,6 +2097,13 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null, $nextArg = ''; } } + } elseif (($arg['type'] ?? '') === 'Error') { + $argValue = $arg['value']; + if (is_scalar($argValue)) { + $nextArg = $argValue; + } elseif (empty($argValue)) { + $nextArg = ''; + } } $args[] = $nextArg; if ($functionName !== 'MKMATRIX') { @@ -2217,11 +2234,13 @@ private function processTokenStack(false|array $tokens, ?string $cellID = null, } } if ($namedRange === null) { - return $this->raiseFormulaError("undefined name '$definedName'"); + $result = ExcelError::NAME(); + $stack->push('Error', $result, null); + $this->debugLog->writeDebugLog("Error $result"); + } else { + $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== ''); } - $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== ''); - if (isset($storeKey)) { $branchStore[$storeKey] = $result; } @@ -2487,6 +2506,8 @@ protected function raiseFormulaError(string $errorMessage, int $code = 0, ?Throw $this->formulaError = $errorMessage; $this->cyclicReferenceStack->clear(); $suppress = $this->suppressFormulaErrors; + $suppressed = $suppress ? ' $suppressed' : ''; + $this->debugLog->writeDebugLog("Raise Error$suppressed $errorMessage"); if (!$suppress) { throw new Exception($errorMessage, $code, $exception); } @@ -2788,7 +2809,13 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh $this->debugLog->writeDebugLog('Evaluation Result for Named %s %s is %s', $definedNameType, $namedRange->getName(), $this->showTypeDetails($result)); } - $stack->push('Defined Name', $result, $namedRange->getName()); + $y = $namedRange->getWorksheet()?->getTitle(); + $x = $namedRange->getLocalOnly(); + if ($x && $y !== null) { + $stack->push('Defined Name', $result, "'$y'!" . $namedRange->getName()); + } else { + $stack->push('Defined Name', $result, $namedRange->getName()); + } return $result; } diff --git a/src/PhpSpreadsheet/Calculation/Information/Value.php b/src/PhpSpreadsheet/Calculation/Information/Value.php index afb34a350b..57a6704df1 100644 --- a/src/PhpSpreadsheet/Calculation/Information/Value.php +++ b/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -226,8 +227,15 @@ public static function isFormula(mixed $cellReference = '', ?Cell $cell = null): $worksheet = (!empty($worksheetName)) ? $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheetName) : $cell->getWorksheet(); + if ($worksheet === null) { + return ExcelError::REF(); + } - return ($worksheet !== null) ? $worksheet->getCell($fullCellReference)->isFormula() : ExcelError::REF(); + try { + return $worksheet->getCell($fullCellReference)->isFormula(); + } catch (SpreadsheetException) { + return true; + } } /** diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php index 2c13b96c8f..caf098314b 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php @@ -3,9 +3,11 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class RowColumnInformation @@ -40,9 +42,9 @@ private static function cellColumn(?Cell $cell): int * * @param null|mixed[]|string $cellAddress A reference to a range of cells for which you want the column numbers * - * @return int|int[] + * @return int|int[]|string */ - public static function COLUMN($cellAddress = null, ?Cell $cell = null): int|array + public static function COLUMN($cellAddress = null, ?Cell $cell = null): int|string|array { if (self::cellAddressNullOrWhitespace($cellAddress)) { return self::cellColumn($cell); @@ -79,7 +81,11 @@ public static function COLUMN($cellAddress = null, ?Cell $cell = null): int|arra $cellAddress = (string) preg_replace('/[^a-z]/i', '', $cellAddress); - return Coordinate::columnIndexFromString($cellAddress); + try { + return Coordinate::columnIndexFromString($cellAddress); + } catch (SpreadsheetException) { + return ExcelError::NAME(); + } } /** @@ -100,6 +106,9 @@ public static function COLUMNS($cellAddress = null) if (self::cellAddressNullOrWhitespace($cellAddress)) { return 1; } + if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) { + return $cellAddress; + } if (!is_array($cellAddress)) { return ExcelError::VALUE(); } @@ -115,9 +124,18 @@ public static function COLUMNS($cellAddress = null) return $columns; } - private static function cellRow(?Cell $cell): int + private static function cellRow(?Cell $cell): int|string + { + return ($cell !== null) ? self::convert0ToName($cell->getRow()) : 1; + } + + private static function convert0ToName(int|string $result): int|string { - return ($cell !== null) ? $cell->getRow() : 1; + if (is_int($result) && ($result <= 0 || $result > 1048576)) { + return ExcelError::NAME(); + } + + return $result; } /** @@ -135,9 +153,9 @@ private static function cellRow(?Cell $cell): int * * @param null|mixed[][]|string $cellAddress A reference to a range of cells for which you want the row numbers * - * @return int|mixed[] + * @return int|mixed[]|string */ - public static function ROW($cellAddress = null, ?Cell $cell = null): int|array + public static function ROW($cellAddress = null, ?Cell $cell = null): int|string|array { if (self::cellAddressNullOrWhitespace($cellAddress)) { return self::cellRow($cell); @@ -172,7 +190,7 @@ public static function ROW($cellAddress = null, ?Cell $cell = null): int|array } [$cellAddress] = explode(':', $cellAddress); - return (int) preg_replace('/\D/', '', $cellAddress); + return self::convert0ToName((int) preg_replace('/\D/', '', $cellAddress)); } /** @@ -193,6 +211,9 @@ public static function ROWS($cellAddress = null) if (self::cellAddressNullOrWhitespace($cellAddress)) { return 1; } + if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) { + return $cellAddress; + } if (!is_array($cellAddress)) { return ExcelError::VALUE(); } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 82de53542e..7c02457a0b 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -691,9 +691,9 @@ public function getAllSheets(): array */ public function getSheetByName(string $worksheetName): ?Worksheet { - $trimWorksheetName = trim($worksheetName, "'"); + $trimWorksheetName = StringHelper::strToUpper(trim($worksheetName, "'")); foreach ($this->workSheetCollection as $worksheet) { - if (strcasecmp($worksheet->getTitle(), $trimWorksheetName) === 0) { + if (StringHelper::strToUpper($worksheet->getTitle()) === $trimWorksheetName) { return $worksheet; } } @@ -1017,13 +1017,34 @@ public function getDefinedName(string $definedName, ?Worksheet $worksheet = null if ($definedName !== '') { $definedName = StringHelper::strToUpper($definedName); // first look for global defined name - if (isset($this->definedNames[$definedName])) { - $returnValue = $this->definedNames[$definedName]; + foreach ($this->definedNames as $dn) { + $upper = StringHelper::strToUpper($dn->getName()); + if ( + !$dn->getLocalOnly() + && $definedName === $upper + ) { + $returnValue = $dn; + + break; + } } // then look for local defined name (has priority over global defined name if both names exist) - if (($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) { - $returnValue = $this->definedNames[$worksheet->getTitle() . '!' . $definedName]; + if ($worksheet !== null) { + $wsTitle = StringHelper::strToUpper($worksheet->getTitle()); + $definedName = (string) preg_replace('/^.*!/', '', $definedName); + foreach ($this->definedNames as $dn) { + $sheet = $dn->getScope() ?? $dn->getWorksheet(); + $upper = StringHelper::strToUpper($dn->getName()); + $upperTitle = StringHelper::strToUpper((string) $sheet?->getTitle()); + if ( + $dn->getLocalOnly() + && $upper === $definedName + && $upperTitle === $wsTitle + ) { + return $dn; + } + } } } diff --git a/tests/PhpSpreadsheetTests/Calculation/DefinedNamesCalculationTest.php b/tests/PhpSpreadsheetTests/Calculation/DefinedNamesCalculationTest.php index e7d44761ac..763c5217aa 100644 --- a/tests/PhpSpreadsheetTests/Calculation/DefinedNamesCalculationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/DefinedNamesCalculationTest.php @@ -21,6 +21,24 @@ public function testNamedRangeCalculations1(string $cellAddress, float $expected $calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue(); self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}"); + $spreadsheet->disconnectWorksheets(); + } + + public function testNamedRangeCalculationsIfError(): void + { + $inputFileType = 'Xlsx'; + $inputFileName = __DIR__ . '/../../data/Calculation/DefinedNames/NamedRanges.xlsx'; + + $reader = IOFactory::createReader($inputFileType); + $spreadsheet = $reader->load($inputFileName); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('E1') + ->setValue('=IFERROR(CHARGE_RATE, 999)'); + $sheet->getCell('F1') + ->setValue('=IFERROR(CHARGE_RATX, 999)'); + self::assertSame(7.5, $sheet->getCell('E1')->getCalculatedValue()); + self::assertSame(999, $sheet->getCell('F1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); } #[DataProvider('namedRangeCalculationProvider2')] @@ -36,6 +54,7 @@ public function testNamedRangeCalculationsWithAdjustedRateValue(string $cellAddr $calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue(); self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}"); + $spreadsheet->disconnectWorksheets(); } #[DataProvider('namedRangeCalculationProvider1')] @@ -49,6 +68,7 @@ public function testNamedFormulaCalculations1(string $cellAddress, float $expect $calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue(); self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}"); + $spreadsheet->disconnectWorksheets(); } #[DataProvider('namedRangeCalculationProvider2')] @@ -64,6 +84,7 @@ public function testNamedFormulaeCalculationsWithAdjustedRateValue(string $cellA $calculatedCellValue = $spreadsheet->getActiveSheet()->getCell($cellAddress)->getCalculatedValue(); self::assertSame($expectedValue, $calculatedCellValue, "Failed calculation for cell {$cellAddress}"); + $spreadsheet->disconnectWorksheets(); } public static function namedRangeCalculationProvider1(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsFormulaTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsFormulaTest.php index 8fc08c7988..2b5e0953c8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsFormulaTest.php @@ -81,7 +81,9 @@ public function testIsFormula(): void $sheet1->getCell('G5')->setValue('=ISFORMULA(range2f)'); $sheet1->getCell('G6')->setValue('=ISFORMULA(range2t)'); $sheet1->getCell('G7')->setValue('=ISFORMULA(range2ft)'); - self::assertSame('#NAME?', $sheet1->getCell('G1')->getCalculatedValue()); + self::assertTrue( + $sheet1->getCell('G1')->getCalculatedValue() + ); self::assertFalse($sheet1->getCell('G3')->getCalculatedValue()); self::assertTrue($sheet1->getCell('G4')->getCalculatedValue()); self::assertFalse($sheet1->getCell('G5')->getCalculatedValue()); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php index 939aa3e33e..e284578e25 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php @@ -36,7 +36,7 @@ public static function providerIsRef(): array 'quoted sheet name' => [true, "'Worksheet2'!B1:B2"], 'quoted sheet name with apostrophe' => [true, "'Work''sheet2'!B1:B2"], 'named range' => [true, 'NAMED_RANGE'], - 'unknown named range' => ['#NAME?', 'xNAMED_RANGE'], + 'unknown named range' => [false, 'xNAMED_RANGE'], 'indirect to a cell reference' => [true, 'INDIRECT("A1")'], 'indirect to a worksheet/cell reference' => [true, 'INDIRECT("\'Worksheet\'!A1")'], 'indirect to invalid worksheet/cell reference' => [false, 'INDIRECT("\'Invalid Worksheet\'!A1")'], diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php index d019d428b8..97b25fa124 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php @@ -21,6 +21,7 @@ public function testColumnOnSpreadsheet(mixed $expectedResult, string $cellRefer $sheet1 = $this->getSpreadsheet()->createSheet(); $sheet1->setTitle('OtherSheet'); + $this->getSpreadsheet()->addNamedRange(new NamedRange('localname', $sheet1, '$F$6:$H$6', true)); if ($cellReference === 'omitted') { $sheet->getCell('B3')->setValue('=COLUMN()'); @@ -61,7 +62,7 @@ public function testCOLUMNSheetWithApostrophe(): void $sheet = $this->getSheet(); $sheet1 = $this->getSpreadsheet()->createSheet(); - $sheet1->setTitle("apo''strophe"); + $sheet1->setTitle("apo'strophe"); $this->getSpreadsheet()->addNamedRange(new NamedRange('newnr', $sheet1, '$F$5:$H$5', true)); // defined locally, only usable on sheet1 $sheet1->getCell('B3')->setValue('=COLUMN(newnr)'); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue1637Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue1637Test.php new file mode 100644 index 0000000000..f5a1ee6f88 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue1637Test.php @@ -0,0 +1,26 @@ +load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame( + '=IFERROR(__xludf.DUMMYFUNCTION("flatten(A1:A5, B1:B5)"),1.0)', + $sheet->getCell('C1')->getValue() + ); + self::assertSame(1.0, $sheet->getCell('C1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php b/tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php index 76505334ba..01026d700c 100644 --- a/tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php +++ b/tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php @@ -17,5 +17,5 @@ 'unknown name' => ['#NAME?', 'namedrange2'], 'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'], 'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'], - //'qualified out of scope $f$6:$h$6' => [3, 'OtherSheet!localname'], // needs investigation + 'qualified out of scope $f$6:$h$6' => [3, 'OtherSheet!localname'], ]; diff --git a/tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php b/tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php index 16a43707cb..8c0d787c02 100644 --- a/tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php +++ b/tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php @@ -15,5 +15,5 @@ 'unknown name' => ['#NAME?', 'namedrange2'], 'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'], 'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'], - //'qualified name' => [6, 'OtherSheet!localname'], // Never reaches function + 'qualified name' => [6, 'OtherSheet!localname'], ]; diff --git a/tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php b/tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php index 0c0936c86d..93b65ab1eb 100644 --- a/tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php +++ b/tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php @@ -17,5 +17,5 @@ 'unknown name' => ['#NAME?', 'InvalidCellAddress'], 'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'], 'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'], - //'qualified out of scope $F$6:$H$6' => [1, 'OtherSheet!localname'], // needs investigation + 'qualified out of scope $F$6:$H$6' => [1, 'OtherSheet!localname'], ]; diff --git a/tests/data/Calculation/LookupRef/ROWonSpreadsheet.php b/tests/data/Calculation/LookupRef/ROWonSpreadsheet.php index 0250934df0..f69e261213 100644 --- a/tests/data/Calculation/LookupRef/ROWonSpreadsheet.php +++ b/tests/data/Calculation/LookupRef/ROWonSpreadsheet.php @@ -16,5 +16,5 @@ 'unknown name' => ['#NAME?', 'namedrange2'], 'unknown name as first part of range' => ['#NAME?', 'InvalidCell:A2'], 'unknown name as second part of range' => ['#NAME?', 'A2:InvalidCell'], - //'qualified name' => [6, 'OtherSheet!localname'], // Never reaches function + 'qualified name' => [6, 'OtherSheet!localname'], ]; diff --git a/tests/data/Reader/XLSX/issue.1637.xlsx b/tests/data/Reader/XLSX/issue.1637.xlsx new file mode 100644 index 0000000000..be55779fe4 Binary files /dev/null and b/tests/data/Reader/XLSX/issue.1637.xlsx differ