From ac148c8d10b60475d0db8d258129b6e45805ec4a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 12 Feb 2025 08:05:11 -0800 Subject: [PATCH 1/9] Xlsx Reader Defined Name on Sheet with Apostrophe in Title Fix #4356. Xlsx Reader needs to handle apostrophe for sheet title in defined name by converting doubled apostrophes to single. --- src/PhpSpreadsheet/Reader/Xlsx.php | 5 ++- .../Reader/Xlsx/Issue4356Test.php | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index e7cd0b711..5840cfd91 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1820,7 +1820,10 @@ class Xlsx extends BaseReader if (is_array($definedNameValueParts)) { // Extract sheet name [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); - $extractedSheetName = trim((string) $extractedSheetName, "'"); + $extractedSheetName ??= ''; + if ($extractedSheetName[0] === "'" && substr($extractedSheetName, -1) === "'") { + $extractedSheetName = str_replace("''", "'", substr($extractedSheetName, 1, -1)); + } // Locate sheet $locatedSheet = $excel->getSheetByName($extractedSheetName); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php new file mode 100644 index 000000000..374ae7449 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php @@ -0,0 +1,32 @@ +getActiveSheet(); + $originalSheet->setTitle("Goodn't sheet name"); + $originalSpreadsheet->addNamedRange( + new NamedRange('CELLNAME', $originalSheet, '$A$1') + ); + $originalSheet->setCellValue('A1', 'This is a named cell.'); + $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); + $originalSpreadsheet->disconnectWorksheets(); + + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('C1', '=CELLNAME'); + self::assertSame('This is a named cell.', $sheet->getCell('C1')->getCalculatedValue()); + + $spreadsheet->disconnectWorksheets(); + } +} From 7b606243a022d7d626d1504733776e2d17164c1b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 14 Feb 2025 19:40:34 -0800 Subject: [PATCH 2/9] More Apostrophe Fixes Fix #4362. A similar problem to 4360, Style not handling sheet name with embedded apostrophe properly. And, with two examples in hand, I was able to determine a pattern to find and fix other possible exposures. --- .../Calculation/Calculation.php | 14 +++-- .../Calculation/Information/Value.php | 2 +- .../Internal/ExcelArrayPseudoFunctions.php | 4 +- .../Calculation/LookupRef/Helpers.php | 3 +- .../Calculation/LookupRef/Offset.php | 3 +- src/PhpSpreadsheet/Chart/DataSeriesValues.php | 2 +- src/PhpSpreadsheet/Reader/Gnumeric.php | 3 +- .../Reader/Ods/DefinedNames.php | 2 +- .../Reader/Xls/LoadSpreadsheet.php | 17 +++--- src/PhpSpreadsheet/Reader/Xlsx.php | 8 +-- src/PhpSpreadsheet/Style/Style.php | 4 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 61 ++++++++++++------- src/PhpSpreadsheet/Writer/Xls/Parser.php | 4 +- src/PhpSpreadsheet/Writer/Xlsx/AutoFilter.php | 2 +- .../Calculation/ParseFormulaTest.php | 12 ++++ .../Functional/PrintAreaTest.php | 5 +- .../Reader/Xls/Issue4356Test.php | 42 +++++++++++++ .../Reader/Xlsx/Issue4356Test.php | 8 +++ 18 files changed, 136 insertions(+), 60 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/Issue4356Test.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index effc63a48..5d45e1436 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4408,7 +4408,9 @@ class Calculation if ($rangeWS1 !== '') { $rangeWS1 .= '!'; } - $rangeSheetRef = trim($rangeSheetRef, "'"); + if (str_starts_with($rangeSheetRef, "'")) { + $rangeSheetRef = Worksheet::unApostrophizeTitle($rangeSheetRef); + } [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true); if ($rangeWS2 !== '') { $rangeWS2 .= '!'; @@ -4766,18 +4768,18 @@ class Calculation } } if (str_contains($operand1Data['reference'] ?? '', '!')) { - [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true); + [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true, true); } else { $sheet1 = ($pCellWorksheet !== null) ? $pCellWorksheet->getTitle() : ''; } $sheet1 ??= ''; - [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true); + [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true, true); if (empty($sheet2)) { $sheet2 = $sheet1; } - if (trim($sheet1, "'") === trim($sheet2, "'")) { + if ($sheet1 === $sheet2) { if ($operand1Data['reference'] === null && $cell !== null) { if (is_array($operand1Data['value'])) { $operand1Data['reference'] = $cell->getCoordinate(); @@ -5495,7 +5497,7 @@ class Calculation $worksheetName = $worksheet->getTitle(); if (str_contains($range, '!')) { - [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true); + [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true, true); $worksheet = ($this->spreadsheet === null) ? null : $this->spreadsheet->getSheetByName($worksheetName); } @@ -5557,7 +5559,7 @@ class Calculation if ($worksheet !== null) { if (str_contains($range, '!')) { - [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true); + [$worksheetName, $range] = Worksheet::extractSheetTitle($range, true, true); $worksheet = ($this->spreadsheet === null) ? null : $this->spreadsheet->getSheetByName($worksheetName); } diff --git a/src/PhpSpreadsheet/Calculation/Information/Value.php b/src/PhpSpreadsheet/Calculation/Information/Value.php index 49361ef06..18274651f 100644 --- a/src/PhpSpreadsheet/Calculation/Information/Value.php +++ b/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -45,7 +45,7 @@ class Value $cellValue = Functions::trimTrailingRange($value); if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/ui', $cellValue) === 1) { - [$worksheet, $cellValue] = Worksheet::extractSheetTitle($cellValue, true); + [$worksheet, $cellValue] = Worksheet::extractSheetTitle($cellValue, true, true); if (!empty($worksheet) && $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheet) === null) { return false; } diff --git a/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php b/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php index 2bccab8da..83ea458a2 100644 --- a/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php +++ b/src/PhpSpreadsheet/Calculation/Internal/ExcelArrayPseudoFunctions.php @@ -15,7 +15,7 @@ class ExcelArrayPseudoFunctions { $worksheet = $cell->getWorksheet(); - [$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true); + [$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true, true); if (preg_match('/^([$]?[a-z]{1,3})([$]?([0-9]{1,7})):([$]?[a-z]{1,3})([$]?([0-9]{1,7}))$/i', "$referenceCellCoordinate", $matches) === 1) { $ourRow = $cell->getRow(); $firstRow = (int) $matches[3]; @@ -44,7 +44,7 @@ class ExcelArrayPseudoFunctions //$coordinate = $cell->getCoordinate(); $worksheet = $cell->getWorksheet(); - [$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true); + [$referenceWorksheetName, $referenceCellCoordinate] = Worksheet::extractSheetTitle($cellReference, true, true); $referenceCell = ($referenceWorksheetName === '') ? $worksheet->getCell((string) $referenceCellCoordinate) : $worksheet->getParentOrThrow() diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php index 191144bfd..21c2030cd 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -61,8 +61,7 @@ class Helpers { $sheetName = ''; if (str_contains($cellAddress, '!')) { - [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); - $sheetName = trim($sheetName, "'"); + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true, true); } $worksheet = ($sheetName !== '') diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php index 260ccc3a5..c3643cd65 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -103,8 +103,7 @@ class Offset $sheetName = ''; if (str_contains($cellAddress, '!')) { - [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); - $sheetName = trim($sheetName, "'"); + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true, true); } $worksheet = ($sheetName !== '') diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 70f90bf78..6da4bc4aa 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -446,7 +446,7 @@ class DataSeriesValues extends Properties } unset($dataValue); } else { - [$worksheet, $cellRange] = Worksheet::extractSheetTitle($this->dataSource, true); + [, $cellRange] = Worksheet::extractSheetTitle($this->dataSource, true); $dimensions = Coordinate::rangeDimension(str_replace('$', '', $cellRange ?? '')); if (($dimensions[0] == 1) || ($dimensions[1] == 1)) { $this->dataValues = Functions::flattenArray($newDataValues); diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index ed81efb22..576fa3bc4 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -517,8 +517,7 @@ class Gnumeric extends BaseReader continue; } - [$worksheetName] = Worksheet::extractSheetTitle($value, true); - $worksheetName = trim($worksheetName, "'"); + [$worksheetName] = Worksheet::extractSheetTitle($value, true, true); $worksheet = $this->spreadsheet->getSheetByName($worksheetName); // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet if ($worksheet !== null) { diff --git a/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php index a99e3ea74..713ea550d 100644 --- a/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php +++ b/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php @@ -60,7 +60,7 @@ class DefinedNames extends BaseLoader */ private function addDefinedName(string $baseAddress, string $definedName, string $value): void { - [$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true); + [$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true, true); $worksheet = $this->spreadsheet->getSheetByName($sheetReference); // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet if ($worksheet !== null) { diff --git a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php index aeda44aa2..7e24e7f11 100644 --- a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php +++ b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php @@ -588,8 +588,8 @@ class LoadSpreadsheet extends Xls // $range should look like one of these // Foo!$C$7:$J$66 // Bar!$A$1:$IV$2 - $explodes = Worksheet::extractSheetTitle($range, true); - $sheetName = trim($explodes[0], "'"); + $explodes = Worksheet::extractSheetTitle($range, true, true); + $sheetName = (string) $explodes[0]; if (!str_contains($explodes[1], ':')) { $explodes[1] = $explodes[1] . ':' . $explodes[1]; } @@ -617,8 +617,9 @@ class LoadSpreadsheet extends Xls // Sheet!$A$1:$B$65536 // Sheet!$A$1:$IV$2 if (str_contains($range, '!')) { - $explodes = Worksheet::extractSheetTitle($range, true); - if ($docSheet = $xls->spreadsheet->getSheetByName($explodes[0])) { + $explodes = Worksheet::extractSheetTitle($range, true, true); + $docSheet = $xls->spreadsheet->getSheetByName($explodes[0]); + if ($docSheet) { $extractedRange = $explodes[1]; $extractedRange = str_replace('$', '', $extractedRange); @@ -646,11 +647,9 @@ class LoadSpreadsheet extends Xls /** @var non-empty-string $formula */ $formula = $definedName['formula']; if (str_contains($formula, '!')) { - $explodes = Worksheet::extractSheetTitle($formula, true); - if ( - ($docSheet = $xls->spreadsheet->getSheetByName($explodes[0])) - || ($docSheet = $xls->spreadsheet->getSheetByName(trim($explodes[0], "'"))) - ) { + $explodes = Worksheet::extractSheetTitle($formula, true, true); + $docSheet = $xls->spreadsheet->getSheetByName($explodes[0]); + if ($docSheet) { $extractedRange = $explodes[1]; $localOnly = ($definedName['scope'] === 0) ? false : true; diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 5840cfd91..4b5e893e0 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1819,14 +1819,10 @@ class Xlsx extends BaseReader $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $extractedRange); if (is_array($definedNameValueParts)) { // Extract sheet name - [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); - $extractedSheetName ??= ''; - if ($extractedSheetName[0] === "'" && substr($extractedSheetName, -1) === "'") { - $extractedSheetName = str_replace("''", "'", substr($extractedSheetName, 1, -1)); - } + [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true, true); // Locate sheet - $locatedSheet = $excel->getSheetByName($extractedSheetName); + $locatedSheet = $excel->getSheetByName("$extractedSheetName"); } } diff --git a/src/PhpSpreadsheet/Style/Style.php b/src/PhpSpreadsheet/Style/Style.php index 68ca39e06..022a49ac7 100644 --- a/src/PhpSpreadsheet/Style/Style.php +++ b/src/PhpSpreadsheet/Style/Style.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Style extends Supervisor { @@ -189,7 +190,8 @@ class Style extends Supervisor // Uppercase coordinate and strip any Worksheet reference from the selected range $pRange = strtoupper($pRange); if (str_contains($pRange, '!')) { - $pRangeWorksheet = StringHelper::strToUpper(trim(substr($pRange, 0, (int) strrpos($pRange, '!')), "'")); + $pRangeWorksheet = StringHelper::strToUpper(substr($pRange, 0, (int) strrpos($pRange, '!'))); + $pRangeWorksheet = Worksheet::unApostrophizeTitle($pRangeWorksheet); if ($pRangeWorksheet !== '' && StringHelper::strToUpper($this->getActiveSheet()->getTitle()) !== $pRangeWorksheet) { throw new Exception('Invalid Worksheet for specified Range'); } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 44ede2add..b1b588f55 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -23,6 +23,7 @@ use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Color; @@ -404,15 +405,15 @@ class Worksheet */ private static function checkSheetCodeName(string $sheetCodeName): string { - $charCount = Shared\StringHelper::countCharacters($sheetCodeName); + $charCount = StringHelper::countCharacters($sheetCodeName); if ($charCount == 0) { throw new Exception('Sheet code name cannot be empty.'); } // Some of the printable ASCII characters are invalid: * : / \ ? [ ] and first and last characters cannot be a "'" if ( (str_replace(self::INVALID_CHARACTERS, '', $sheetCodeName) !== $sheetCodeName) - || (Shared\StringHelper::substring($sheetCodeName, -1, 1) == '\'') - || (Shared\StringHelper::substring($sheetCodeName, 0, 1) == '\'') + || (StringHelper::substring($sheetCodeName, -1, 1) == '\'') + || (StringHelper::substring($sheetCodeName, 0, 1) == '\'') ) { throw new Exception('Invalid character found in sheet code name'); } @@ -440,7 +441,7 @@ class Worksheet } // Enforce maximum characters allowed for sheet title - if (Shared\StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) { + if (StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) { throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.'); } @@ -869,19 +870,19 @@ class Worksheet if ($this->parent->sheetNameExists($title)) { // Use name, but append with lowest possible integer - if (Shared\StringHelper::countCharacters($title) > 29) { - $title = Shared\StringHelper::substring($title, 0, 29); + if (StringHelper::countCharacters($title) > 29) { + $title = StringHelper::substring($title, 0, 29); } $i = 1; while ($this->parent->sheetNameExists($title . ' ' . $i)) { ++$i; if ($i == 10) { - if (Shared\StringHelper::countCharacters($title) > 28) { - $title = Shared\StringHelper::substring($title, 0, 28); + if (StringHelper::countCharacters($title) > 28) { + $title = StringHelper::substring($title, 0, 28); } } elseif ($i == 100) { - if (Shared\StringHelper::countCharacters($title) > 27) { - $title = Shared\StringHelper::substring($title, 0, 27); + if (StringHelper::countCharacters($title) > 27) { + $title = StringHelper::substring($title, 0, 27); } } } @@ -1189,7 +1190,7 @@ class Worksheet // Worksheet reference? if (str_contains($coordinate, '!')) { - $worksheetReference = self::extractSheetTitle($coordinate, true); + $worksheetReference = self::extractSheetTitle($coordinate, true, true); $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]); $finalCoordinate = strtoupper($worksheetReference[1]); @@ -1222,9 +1223,8 @@ class Worksheet if (Coordinate::coordinateIsRange($finalCoordinate)) { throw new Exception('Cell coordinate string can not be a range of cells.'); - } elseif (str_contains($finalCoordinate, '$')) { - throw new Exception('Cell coordinate must not be absolute.'); } + $finalCoordinate = str_replace('$', '', $finalCoordinate); return [$sheet, $finalCoordinate]; } @@ -2047,10 +2047,10 @@ class Worksheet */ protected function getTableIndexByName(string $name): ?int { - $name = Shared\StringHelper::strToUpper($name); + $name = StringHelper::strToUpper($name); foreach ($this->tableCollection as $index => $table) { /** @var Table $table */ - if (Shared\StringHelper::strToUpper($table->getName()) === $name) { + if (StringHelper::strToUpper($table->getName()) === $name) { return $index; } } @@ -3182,7 +3182,7 @@ class Worksheet * * @return ($range is non-empty-string ? ($returnRange is true ? array{0: string, 1: string} : string) : ($returnRange is true ? array{0: null, 1: null} : null)) */ - public static function extractSheetTitle(?string $range, bool $returnRange = false): array|null|string + public static function extractSheetTitle(?string $range, bool $returnRange = false, bool $unapostrophize = false): array|null|string { if (empty($range)) { return $returnRange ? [null, null] : null; @@ -3194,12 +3194,27 @@ class Worksheet } if ($returnRange) { - return [substr($range, 0, $sep), substr($range, $sep + 1)]; + $title = substr($range, 0, $sep); + if ($unapostrophize) { + $title = self::unApostrophizeTitle($title); + } + + return [$title, substr($range, $sep + 1)]; } return substr($range, $sep + 1); } + public static function unApostrophizeTitle(?string $title): string + { + $title ??= ''; + if ($title[0] === "'" && substr($title, -1) === "'") { + $title = str_replace("''", "'", substr($title, 1, -1)); + } + + return $title; + } + /** * Get hyperlink. * @@ -3571,19 +3586,19 @@ class Worksheet if ($this->parent->sheetCodeNameExists($codeName)) { // Use name, but append with lowest possible integer - if (Shared\StringHelper::countCharacters($codeName) > 29) { - $codeName = Shared\StringHelper::substring($codeName, 0, 29); + if (StringHelper::countCharacters($codeName) > 29) { + $codeName = StringHelper::substring($codeName, 0, 29); } $i = 1; while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) { ++$i; if ($i == 10) { - if (Shared\StringHelper::countCharacters($codeName) > 28) { - $codeName = Shared\StringHelper::substring($codeName, 0, 28); + if (StringHelper::countCharacters($codeName) > 28) { + $codeName = StringHelper::substring($codeName, 0, 28); } } elseif ($i == 100) { - if (Shared\StringHelper::countCharacters($codeName) > 27) { - $codeName = Shared\StringHelper::substring($codeName, 0, 27); + if (StringHelper::countCharacters($codeName) > 27) { + $codeName = StringHelper::substring($codeName, 0, 27); } } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 5324a7bf5..9ef52b896 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -671,7 +671,7 @@ class Parser private function convertRange3d(string $token): string { // Split the ref at the ! symbol - [$ext_ref, $range] = PhpspreadsheetWorksheet::extractSheetTitle($token, true); + [$ext_ref, $range] = PhpspreadsheetWorksheet::extractSheetTitle($token, true, true); // Convert the external reference part (different for BIFF8) $ext_ref = $this->getRefIndex($ext_ref ?? ''); @@ -723,7 +723,7 @@ class Parser private function convertRef3d(string $cell): string { // Split the ref at the ! symbol - [$ext_ref, $cell] = PhpspreadsheetWorksheet::extractSheetTitle($cell, true); + [$ext_ref, $cell] = PhpspreadsheetWorksheet::extractSheetTitle($cell, true, true); // Convert the external reference part (different for BIFF8) $ext_ref = $this->getRefIndex($ext_ref ?? ''); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Writer/Xlsx/AutoFilter.php index d96ac096e..50b1a8f6e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/AutoFilter.php @@ -24,7 +24,7 @@ class AutoFilter extends WriterPart $range = Coordinate::splitRange($autoFilterRange); $range = $range[0]; // Strip any worksheet ref - [$ws, $range[0]] = ActualWorksheet::extractSheetTitle($range[0], true); + [, $range[0]] = ActualWorksheet::extractSheetTitle($range[0], true); $range = implode(':', $range); $objWriter->writeAttribute('ref', str_replace('$', '', $range)); diff --git a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php index 4e06f9ca0..c529700ee 100644 --- a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php @@ -175,6 +175,18 @@ class ParseFormulaTest extends TestCase ], "=MIN('sheet1'!A:A) + 'sheet1'!A1", ], + 'Combined Cell Reference and Column Range Unquoted Sheet' => [ + [ + ['type' => 'Column Reference', 'value' => 'sheet1!A1', 'reference' => 'sheet1!A1'], + ['type' => 'Column Reference', 'value' => 'sheet1!A1048576', 'reference' => 'sheet1!A1048576'], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Cell Reference', 'value' => 'sheet1!A1', 'reference' => 'sheet1!A1'], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + '=MIN(sheet1!A:A) + sheet1!A1', + ], 'Combined Cell Reference and Column Range with quote' => [ [ ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], diff --git a/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php b/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php index 2918341eb..02a64eab7 100644 --- a/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php +++ b/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php @@ -6,6 +6,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Functional; use PhpOffice\PhpSpreadsheet\Reader\BaseReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PHPUnit\Framework\Attributes\DataProvider; class PrintAreaTest extends AbstractFunctional { @@ -17,7 +18,7 @@ class PrintAreaTest extends AbstractFunctional ]; } - #[\PHPUnit\Framework\Attributes\DataProvider('providerFormats')] + #[DataProvider('providerFormats')] public function testPageSetup(string $format): void { // Create new workbook with 6 sheets and different print areas @@ -41,6 +42,7 @@ class PrintAreaTest extends AbstractFunctional $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format, function (BaseReader $reader): void { $reader->setLoadSheetsOnly(['Sheet 1', 'Sheet 3', 'Sheet 4', 'Sheet 5', 'Sheet 6']); }); + $spreadsheet->disconnectWorksheets(); $actual1 = self::getPrintArea($reloadedSpreadsheet, 'Sheet 1'); $actual3 = self::getPrintArea($reloadedSpreadsheet, 'Sheet 3'); @@ -52,6 +54,7 @@ class PrintAreaTest extends AbstractFunctional self::assertSame('A4:B4,D1:E4', $actual4, 'should be able to write and read page setup with multiple print areas'); self::assertSame('A1:J10', $actual5, 'add by column and row'); self::assertSame('A1:J10,L1:L10', $actual6, 'multiple add by column and row'); + $reloadedSpreadsheet->disconnectWorksheets(); } private static function getPrintArea(Spreadsheet $spreadsheet, string $name): string diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/Issue4356Test.php b/tests/PhpSpreadsheetTests/Reader/Xls/Issue4356Test.php new file mode 100644 index 000000000..ddcb0dfe4 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/Issue4356Test.php @@ -0,0 +1,42 @@ +getActiveSheet(); + $originalSheet->setTitle("Goodn't sheet name"); + $originalSpreadsheet->addNamedRange( + new NamedRange('CELLNAME', $originalSheet, '$A$1') + ); + $originalSheet->setCellValue('A1', 'This is a named cell.'); + $originalSheet->getStyle('A1')->getFont()->setItalic(true); + $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xls'); + $originalSpreadsheet->disconnectWorksheets(); + + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('C1', '=CELLNAME'); + self::assertSame('This is a named cell.', $sheet->getCell('C1')->getCalculatedValue()); + $namedRange2 = $spreadsheet->getNamedRange('CELLNAME'); + self::assertNotNull($namedRange2); + $sheetx = $namedRange2->getWorksheet(); + self::assertNotNull($sheetx); + $style = $sheetx->getStyle($namedRange2->getRange()); + //$style = $sheetx->getStyle('CELLNAME'); // no exception but doesn't work + self::assertTrue($style->getFont()->getItalic()); + $style->getFont()->setItalic(false); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php index 374ae7449..728751145 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php @@ -20,12 +20,20 @@ class Issue4356Test extends AbstractFunctional new NamedRange('CELLNAME', $originalSheet, '$A$1') ); $originalSheet->setCellValue('A1', 'This is a named cell.'); + $originalSheet->getStyle('A1')->getFont()->setItalic(true); $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); $originalSpreadsheet->disconnectWorksheets(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setCellValue('C1', '=CELLNAME'); self::assertSame('This is a named cell.', $sheet->getCell('C1')->getCalculatedValue()); + $namedRange2 = $spreadsheet->getNamedRange('CELLNAME'); + self::assertNotNull($namedRange2); + $sheetx = $namedRange2->getWorksheet(); + self::assertNotNull($sheetx); + $style = $sheetx->getStyle($namedRange2->getRange()); + self::assertTrue($style->getFont()->getItalic()); + $style->getFont()->setItalic(false); $spreadsheet->disconnectWorksheets(); } From 4e7e876b830e2e2eb87e6fe8787fd88bbbd29881 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 16 Feb 2025 17:24:17 -0800 Subject: [PATCH 3/9] Tighten Up getStyle(definedName) Processing --- src/PhpSpreadsheet/Worksheet/Validations.php | 14 +++++++++----- src/PhpSpreadsheet/Worksheet/Worksheet.php | 3 +++ .../Reader/Xls/Issue4356Test.php | 14 ++++++++------ .../Reader/Xlsx/Issue4356Test.php | 13 ++++++++----- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index e7ecdf5f3..a873c5eaa 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\CellAddress; use PhpOffice\PhpSpreadsheet\Cell\CellRange; @@ -45,7 +46,7 @@ class Validations if (is_string($cellRange) || is_numeric($cellRange)) { // Convert a single column reference like 'A' to 'A:A', // a single row reference like '1' to '1:1' - $cellRange = (string) preg_replace('/^([A-Z]+|\d+)$/', '${1}:${1}', (string) $cellRange); + $cellRange = Preg::replace('/^([A-Z]+|\d+)$/', '${1}:${1}', (string) $cellRange); } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { $cellRange = new CellRange($cellRange, $cellRange); } @@ -62,7 +63,7 @@ class Validations */ public static function convertWholeRowColumn(?string $addressRange): string { - return (string) preg_replace( + return Preg::replace( ['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'], [self::SETMAXROW, self::SETMAXCOL], $addressRange ?? '' @@ -109,16 +110,19 @@ class Validations return (string) $cellRange; } - public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet): string + public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet, bool $replaceDollar = false): string { // Uppercase coordinate $coordinate = strtoupper($coordinate); // Eliminate leading equal sign - $testCoordinate = (string) preg_replace('/^=/', '', $coordinate); + $testCoordinate = Preg::replace('/^=/', '', $coordinate); $defined = $worksheet->getParentOrThrow()->getDefinedName($testCoordinate, $worksheet); if ($defined !== null) { if ($defined->getWorksheet() === $worksheet && !$defined->isFormula()) { - $coordinate = (string) preg_replace('/^=/', '', $defined->getValue()); + $coordinate = Preg::replace('/^=/', '', $defined->getValue()); + if ($replaceDollar) { + $coordinate = str_replace('$', '', $coordinate); + } } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index b1b588f55..3f47a2b17 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1395,6 +1395,9 @@ class Worksheet */ public function getStyle(AddressRange|CellAddress|int|string|array $cellCoordinate): Style { + if (is_string($cellCoordinate)) { + $cellCoordinate = Validations::definedNameToCoordinate($cellCoordinate, $this, true); + } $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate); // set this sheet as active diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/Issue4356Test.php b/tests/PhpSpreadsheetTests/Reader/Xls/Issue4356Test.php index ddcb0dfe4..4ac508dd2 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/Issue4356Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/Issue4356Test.php @@ -14,26 +14,28 @@ class Issue4356Test extends AbstractFunctional { // Reader couldn't handle sheet title with apostrophe for defined name. // Issue was reported against Xlsx - see how Xls does. + $nameDefined = 'CELLNAME'; $originalSpreadsheet = new Spreadsheet(); $originalSheet = $originalSpreadsheet->getActiveSheet(); $originalSheet->setTitle("Goodn't sheet name"); $originalSpreadsheet->addNamedRange( - new NamedRange('CELLNAME', $originalSheet, '$A$1') + new NamedRange($nameDefined, $originalSheet, '$A$1') ); $originalSheet->setCellValue('A1', 'This is a named cell.'); - $originalSheet->getStyle('A1')->getFont()->setItalic(true); + $originalSheet->getStyle($nameDefined) + ->getFont() + ->setItalic(true); $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xls'); $originalSpreadsheet->disconnectWorksheets(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->setCellValue('C1', '=CELLNAME'); + $sheet->setCellValue('C1', "=$nameDefined"); self::assertSame('This is a named cell.', $sheet->getCell('C1')->getCalculatedValue()); - $namedRange2 = $spreadsheet->getNamedRange('CELLNAME'); + $namedRange2 = $spreadsheet->getNamedRange($nameDefined); self::assertNotNull($namedRange2); $sheetx = $namedRange2->getWorksheet(); self::assertNotNull($sheetx); - $style = $sheetx->getStyle($namedRange2->getRange()); - //$style = $sheetx->getStyle('CELLNAME'); // no exception but doesn't work + $style = $sheetx->getStyle($nameDefined); self::assertTrue($style->getFont()->getItalic()); $style->getFont()->setItalic(false); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php index 728751145..16bb8e12f 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4356Test.php @@ -13,25 +13,28 @@ class Issue4356Test extends AbstractFunctional public function testIssue4356(): void { // Reader couldn't handle sheet title with apostrophe for defined name + $nameDefined = 'CELLNAME'; $originalSpreadsheet = new Spreadsheet(); $originalSheet = $originalSpreadsheet->getActiveSheet(); $originalSheet->setTitle("Goodn't sheet name"); $originalSpreadsheet->addNamedRange( - new NamedRange('CELLNAME', $originalSheet, '$A$1') + new NamedRange($nameDefined, $originalSheet, '$A$1') ); $originalSheet->setCellValue('A1', 'This is a named cell.'); - $originalSheet->getStyle('A1')->getFont()->setItalic(true); + $originalSheet->getStyle($nameDefined) + ->getFont() + ->setItalic(true); $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); $originalSpreadsheet->disconnectWorksheets(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->setCellValue('C1', '=CELLNAME'); + $sheet->setCellValue('C1', "=$nameDefined"); self::assertSame('This is a named cell.', $sheet->getCell('C1')->getCalculatedValue()); - $namedRange2 = $spreadsheet->getNamedRange('CELLNAME'); + $namedRange2 = $spreadsheet->getNamedRange($nameDefined); self::assertNotNull($namedRange2); $sheetx = $namedRange2->getWorksheet(); self::assertNotNull($sheetx); - $style = $sheetx->getStyle($namedRange2->getRange()); + $style = $sheetx->getStyle($nameDefined); self::assertTrue($style->getFont()->getItalic()); $style->getFont()->setItalic(false); From 8930e634a1fbae710cc70c0edcba93cbe22643ed Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:58:17 -0800 Subject: [PATCH 4/9] Add Some Tests --- .../Calculation/LookupRef/Offset.php | 2 +- src/PhpSpreadsheet/Worksheet/Validations.php | 5 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 3 +- .../Functions/Information/IsRefTest.php | 61 ++++++++---------- .../LookupRef/ColumnOnSpreadsheetTest.php | 18 ++++++ .../Functions/LookupRef/OffsetTest.php | 18 +++++- .../Reader/Ods/DefinedNamesTest.php | 22 +++++++ .../Reader/Ods/DefinedNames.apostrophe.ods | Bin 0 -> 3174 bytes 8 files changed, 89 insertions(+), 40 deletions(-) create mode 100644 tests/data/Reader/Ods/DefinedNames.apostrophe.ods diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php index c3643cd65..69974ff58 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -103,7 +103,7 @@ class Offset $sheetName = ''; if (str_contains($cellAddress, '!')) { - [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true, true); + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); } $worksheet = ($sheetName !== '') diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index 303e016ea..4a8720dbb 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -110,7 +110,7 @@ class Validations return (string) $cellRange; } - public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet, bool $replaceDollar = false): string + public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet): string { // Uppercase coordinate $coordinate = strtoupper($coordinate); @@ -120,9 +120,6 @@ class Validations if ($defined !== null) { if ($defined->getWorksheet() === $worksheet && !$defined->isFormula()) { $coordinate = Preg::replace('/^=/', '', $defined->getValue()); - if ($replaceDollar) { - $coordinate = str_replace('$', '', $coordinate); - } } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 2577044de..b992b5e88 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1396,9 +1396,10 @@ class Worksheet public function getStyle(AddressRange|CellAddress|int|string|array $cellCoordinate): Style { if (is_string($cellCoordinate)) { - $cellCoordinate = Validations::definedNameToCoordinate($cellCoordinate, $this, true); + $cellCoordinate = Validations::definedNameToCoordinate($cellCoordinate, $this); } $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate); + $cellCoordinate = str_replace('$', '', $cellCoordinate); // set this sheet as active $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this)); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php index da977cf91..939aa3e33 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Information/IsRefTest.php @@ -6,46 +6,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Information; use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef\AllSetupTeardown; +use PHPUnit\Framework\Attributes\DataProvider; class IsRefTest extends AllSetupTeardown { - private bool $skipA13 = true; - - public function testIsRef(): void + #[DataProvider('providerIsRef')] + public function testIsRef(mixed $expected, string $ref): void { + if ($expected === 'incomplete') { + self::markTestIncomplete('Calculation is too complicated'); + } $sheet = $this->getSheet(); $sheet->getParentOrThrow()->addDefinedName(new NamedRange('NAMED_RANGE', $sheet, 'C1')); + $sheet->getCell('A1')->setValue("=ISREF($ref)"); + self::assertSame($expected, $sheet->getCell('A1')->getCalculatedValue()); + } - $sheet->getCell('A1')->setValue('=ISREF(B1)'); - $sheet->getCell('A2')->setValue('=ISREF(B1:B2)'); - $sheet->getCell('A3')->setValue('=ISREF(B1:D4 C1:C5)'); - $sheet->getCell('A4')->setValue('=ISREF("PHP")'); - $sheet->getCell('A5')->setValue('=ISREF(B1*B2)'); - $sheet->getCell('A6')->setValue('=ISREF(Worksheet2!B1)'); - $sheet->getCell('A7')->setValue('=ISREF(NAMED_RANGE)'); - $sheet->getCell('A8')->setValue('=ISREF(INDIRECT("' . $sheet->getTitle() . '" & "!" & "A1"))'); - $sheet->getCell('A9')->setValue('=ISREF(INDIRECT("A1"))'); - $sheet->getCell('A10')->setValue('=ISREF(INDIRECT("Invalid Worksheet" & "!" & "A1"))'); - $sheet->getCell('A11')->setValue('=ISREF(INDIRECT("Invalid Worksheet" & "!A1"))'); - $sheet->getCell('A12')->setValue('=ISREF(ZZZ1)'); - $sheet->getCell('A13')->setValue('=ISREF(CHOOSE(2, A1, B1, C1))'); - - self::assertTrue($sheet->getCell('A1')->getCalculatedValue()); // Cell Reference - self::assertTrue($sheet->getCell('A2')->getCalculatedValue()); // Cell Range - self::assertTrue($sheet->getCell('A3')->getCalculatedValue()); // Complex Cell Range - self::assertFalse($sheet->getCell('A4')->getCalculatedValue()); // Text String - self::assertFalse($sheet->getCell('A5')->getCalculatedValue()); // Result of a math expression - self::assertTrue($sheet->getCell('A6')->getCalculatedValue()); // Cell Reference with worksheet - self::assertTrue($sheet->getCell('A7')->getCalculatedValue()); // Named Range - self::assertTrue($sheet->getCell('A8')->getCalculatedValue()); // Indirect to a Cell Reference - self::assertTrue($sheet->getCell('A9')->getCalculatedValue()); // Indirect to a Worksheet/Cell Reference - self::assertFalse($sheet->getCell('A10')->getCalculatedValue()); // Indirect to an Invalid Worksheet/Cell Reference - self::assertFalse($sheet->getCell('A11')->getCalculatedValue()); // Indirect to an Invalid Worksheet/Cell Reference - self::assertFalse($sheet->getCell('A12')->getCalculatedValue()); // Invalid Cell Reference - if ($this->skipA13) { - self::markTestIncomplete('Calculation for A13 is too complicated'); - } - self::assertTrue($sheet->getCell('A13')->getCalculatedValue()); // returned Cell Reference + public static function providerIsRef(): array + { + return [ + 'cell reference' => [true, 'B1'], + 'invalid cell reference' => [false, 'ZZZ1'], + 'cell range' => [true, 'B1:B2'], + 'complex cell range' => [true, 'B1:D4 C1:C5'], + 'text string' => [false, '"PHP"'], + 'math expression' => [false, 'B1*B2'], + 'unquoted sheet name' => [true, 'Worksheet2!B1'], + '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'], + '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")'], + 'returned cell reference' => ['incomplete', 'CHOOSE(2, A1, B1, C1)'], + ]; } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php index a57b91eb3..d019d428b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php @@ -54,4 +54,22 @@ class ColumnOnSpreadsheetTest extends AllSetupTeardown $result = $sheet->getCell('B3')->getCalculatedValue(); self::assertSame('#NAME?', $result); } + + public function testCOLUMNSheetWithApostrophe(): void + { + $this->setArrayAsValue(); + $sheet = $this->getSheet(); + + $sheet1 = $this->getSpreadsheet()->createSheet(); + $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)'); + $result = $sheet1->getCell('B3')->getCalculatedValue(); + self::assertSame(6, $result); + + $sheet->getCell('B3')->setValue('=COLUMN(newnr)'); + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame('#NAME?', $result); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php index c6a66c4c2..5a6bb93ae 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php @@ -6,10 +6,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\NamedRange; +use PHPUnit\Framework\Attributes\DataProvider; class OffsetTest extends AllSetupTeardown { - #[\PHPUnit\Framework\Attributes\DataProvider('providerOFFSET')] + #[DataProvider('providerOFFSET')] public function testOFFSET(mixed $expectedResult, null|string $cellReference = null): void { $result = LookupRef\Offset::OFFSET($cellReference); @@ -58,4 +59,19 @@ class OffsetTest extends AllSetupTeardown self::assertSame(2, $workSheet->getCell('B2')->getCalculatedValue()); } + + public function testOffsetNamedRangeApostropheSheet(): void + { + $workSheet = $this->getSheet(); + $workSheet->setTitle("apo'strophe"); + $workSheet->setCellValue('A1', 1); + $workSheet->setCellValue('A2', 2); + + $this->getSpreadsheet()->addNamedRange(new NamedRange('demo', $workSheet, '=$A$1')); + + $workSheet->setCellValue('B1', '=demo'); + $workSheet->setCellValue('B2', '=OFFSET(demo, 1, 0)'); + + self::assertSame(2, $workSheet->getCell('B2')->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php index c3d3655e9..2c3a36698 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/DefinedNamesTest.php @@ -31,6 +31,28 @@ class DefinedNamesTest extends TestCase $spreadsheet->disconnectWorksheets(); } + public function testDefinedNamesApostropheValue(): void + { + $filename = 'tests/data/Reader/Ods/DefinedNames.apostrophe.ods'; + $reader = new Ods(); + $spreadsheet = $reader->load($filename); + $calculation = Calculation::getInstance($spreadsheet); + $calculation->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_VALUE + ); + $worksheet = $spreadsheet->getActiveSheet(); + self::assertSame("apo'strophe", $worksheet->getTitle()); + + $firstDefinedNameValue = $worksheet->getCell('First')->getValue(); + $secondDefinedNameValue = $worksheet->getCell('Second')->getValue(); + $calculatedFormulaValue = $worksheet->getCell('B2')->getCalculatedValue(); + + self::assertSame(3, $firstDefinedNameValue); + self::assertSame(4, $secondDefinedNameValue); + self::assertSame(12, $calculatedFormulaValue); + $spreadsheet->disconnectWorksheets(); + } + public function testDefinedNamesArray(): void { $filename = 'tests/data/Reader/Ods/DefinedNames.ods'; diff --git a/tests/data/Reader/Ods/DefinedNames.apostrophe.ods b/tests/data/Reader/Ods/DefinedNames.apostrophe.ods new file mode 100644 index 0000000000000000000000000000000000000000..8b4642addabae8ae522fc250cc1fa89f9b648fa0 GIT binary patch literal 3174 zcmZ`+2{_c>`X5^uYqrR~j3pUElYI#(Tb3gGV9eNO1~Vp&B~3`O80dSOaTTcKR-2z3&SZB}d0(w;B9R=g2ZSzFIU z%33E+t%MKhppa$=efLhl8uA{m_lJ5zX3tpFw@I(&*F$U5E8fQcjC`P}1iauRFx(}Hx=)CIE5GEDr7-;*`V4f_wL$hwM0a0<%WCXUtUe`INM>} zeVfp1lbbIfkkZFAx~2|=@pUF-CHlQkSwSxH4V8aR33pq+5%6TQ*r7(EulP!+2Ec1j zE!+(bPd1%U?NclPd2^V_ns#lkRL_j{x(;;H@p?ry8t30&RYj4)W5kRY&4ZN9lB-m1 z0~ecHny!zZncEd71qn93h^V>!8oaO9m)4P3q@31%n&9=w>8Yb=yHU)_poSD<*Hww$ zu%)Qz2;l13)Sg?KsK27LvxCDkiycwz11EUv*m6KE&u@xVayImt$SclHJFt8CDsgb{ z5sAYB`9JOL#A26WLskuu*nJkWzUq1DqviW<*3%B5);R71!4i6as|4GZvqWmA(i~s=laR7jg_J=11jXUD; z5icF6=y^5n2<3qr5Jm<%;6QwyIdA*dqHeWw;&?V(qxiYI(#{U?d?u$F4xbraCV`wy z7Xy@)O)WURIs1EZJzDlD%DW3pByfpI@$Q;dc2u{FVwY^UH2w~kVZ+;i4LDwZw* zZSNzJb!Q|#<*HNEJ*}Ln=l~Swe5&^Rx^LMsIsoB+7kVxEWANHs>yk`XH^I^7io}Gg zdKfGY<6C#O%bCwE}vPCw<9;z!X4Q~0OJZ~ z?Iw0*zG+lSC{PT4sv>|{>kM32DN)bR(jQHX={3e2ZiPf^w9Xek>YixhJ>p>Y;F%IEufz?vpl&-^4EZ|>=pK+mVLdYFnkmnS{|0F)&%HAP4B?`J(d}ER zZF$pnsNs>5aa9&JaL_e!LPdM3Rv>w>{^5e6(ht4yokM=K(d|k}nYym~bn&)0hD1`v z$i57hXnUY>I>pdFx9C`yUj!Uq0nvb&DhLU1*mqU+J8R2_D5{X9)GxPYt_@fMT|FdK zTbUr0rCf=s86zU~F2JJp@i+oc0|fwONz{OG6Ya~rt}xh5WFlC2Yz<5u{oj>Ud|-yl zOrSu_4>#v5XG)dIUE3Wm@DG1<2CBswA0L;tMm@m!ykiS%@rJQn&~RnIpeWzN1$S!c zRz9+l7c$i3-#MRl#Vk0WgAuip+6n1YwVfzmuf5)|jW2gz+VVbLc}_|Gf)PXC;@>Ln zsTs`iz^iRbQq_c6e4Y0hMJ@ZcZO&ci=n^@VBL!C2#)N{$^Pq;Q`fW8FNqV2>Yn-M} z&o3$OcRmaMxi@AYc0%y!i#NN}b9-Mnp`o3bt7ywJ6imqytTBs8EY&TmSl%9cLuh5dlKenEvxqZj8+mdL3(d3C7tQ}^IF9)NaEq=OZ~R=Kcq)1| z)$a&y7=AJ+`Pgr0ntRD+goNu6B!x~?Y5)buQ(Y417({CQz@>`nw* zayrSt#kHip#!h#1Wbh!i2+kl`5*Da`xcw^Yv80UvcaU1Ns#mE-Vs%yYF^<;kOE>N} zXf>6|udesxjFLz5h^us(mDTeQ7N%v&0W>di_uHqPesPB7ok9)oqAwxr{8nEIi(XGE zocP{md+$@Luc*LJ9j*r*CytNf)~R<#-2OI&-G0NhIZj*QqXz)^Y2Qo6wgyV3mKT&! z9%x@LB=%P_t4cBOV^Co?-ieuM57o!lD$3@s5s2G>j#20_XEo_d-BWs&9%*2cb+ij>D=hV!M|kAqi2|tPT`F+I zw5xG+6)9_flUJKaT$5>xHK37IdH~#bZ}zUom+S z4Ms<|4E!~jQX-RdZJ=&+J=Y%3sY##LlZtOh7Iqv>2q3Juy(4Q$Ihi5v&r_-+?S>pNJ9DUtIEqf`YvlLv%!1eH15bvf z*vU7#zN$ErRh`m=YkjH{fM!0YMY~w$l3o{U5ObIsqo1UPOH#!wG)aL%=lfa3z6MFt zzZ2>eEYmETNFra)$2J_~wjT0a{AjE)q@su?1vzb03G%Tyr9EwO7z}oriTnZn_S@}r z^h&>UEbSZs|E-vhW`CD|>1S*6Bh>%a#J>@?wC-<({3q_uOZzu&m8P)&?F#=B_$O6< s1FLD_^0&nK)5M=4_S?i$R@zJ9{{zn2oaxxn3KOkL&;k;2q%8pd1g58I5&!@I literal 0 HcmV?d00001 From ae6c92b0064df389d6ef0c012ac5ea93242d4267 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:45:05 -0800 Subject: [PATCH 5/9] More Tests --- .../Calculation/LookupRef/Offset.php | 2 +- src/PhpSpreadsheet/Reader/Gnumeric.php | 1 + .../Reader/Gnumeric/DefinedNameTest.php | 32 ++++++++++++++++++ .../Reader/Gnumeric/apostrophe3a.gnumeric | Bin 0 -> 1749 bytes .../Reader/Gnumeric/apostrophe3b.gnumeric | Bin 0 -> 1760 bytes 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Gnumeric/DefinedNameTest.php create mode 100644 tests/data/Reader/Gnumeric/apostrophe3a.gnumeric create mode 100644 tests/data/Reader/Gnumeric/apostrophe3b.gnumeric diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php index 69974ff58..c3643cd65 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -103,7 +103,7 @@ class Offset $sheetName = ''; if (str_contains($cellAddress, '!')) { - [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true, true); } $worksheet = ($sheetName !== '') diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 576fa3bc4..e1430072b 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -517,6 +517,7 @@ class Gnumeric extends BaseReader continue; } + $value = str_replace("\\'", "'", $value); [$worksheetName] = Worksheet::extractSheetTitle($value, true, true); $worksheet = $this->spreadsheet->getSheetByName($worksheetName); // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet diff --git a/tests/PhpSpreadsheetTests/Reader/Gnumeric/DefinedNameTest.php b/tests/PhpSpreadsheetTests/Reader/Gnumeric/DefinedNameTest.php new file mode 100644 index 000000000..4733e564e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Gnumeric/DefinedNameTest.php @@ -0,0 +1,32 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame($sheetName, $sheet->getTitle()); + self::assertSame('=sheet1first', $sheet->getCell('C1')->getValue()); + self::assertSame(1, $sheet->getCell('C1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/Gnumeric/apostrophe3a.gnumeric b/tests/data/Reader/Gnumeric/apostrophe3a.gnumeric new file mode 100644 index 0000000000000000000000000000000000000000..6950033ae59dc4c272ea076204a1de19b381d37a GIT binary patch literal 1749 zcmV;`1}gau)95j^jX~H#gIR8Ae*|u2;LyzKGM$k1@3#ut*7KJ-gj#*%oFYj|f}# z>|1|$@X`LkOHLz zd?mr>6jn?jqt$@=426<%Bs~<7$DZBEw-;ndB*=CTE0uG$!dOlqULzLa$4XX|2bQ*% zQ;_U*p$4n1$Jeku?sgB4?Y`!08z`4jTXgS#LF-qPYPFu!Q%UDms&y}jNaYVR&e-tw z-QZn&FYj{3gr`I~t3SwDNw=^SMVlcPF+@Mr(Y_)P{)PoFHNdeziD7nU)OhWSFRAVLI z%r@~H!ghu!Uq3Ewx)tG z84Mtq_n}pT*KeLXeES5jUJI^$fW68>?aRO?lHwOleE)aGhnC2KoG-ckEKeEAAABv zN0?t>ZUc8e&!vw8Xrar}5>*JN-e}?ngL8j$=bEaxC&>lGzaIs11^3Q}bVIB25fv7l z9tjsxAR-Iy?a8i;Df8ACK-{y{J1B}MSoR*p$)`X3!P%d-l^5|nvxWv`bPXW&-LBqfx*B@A_Fh_Rjn>tGk|i_dUk%2i%SmwQPF!y=hAvwUU2zFuyLnYn0YKbmLCR4H6N;#)yU>U*dnV1>& z7R(cB+AwCr1Lq$5km}iTWhkK_i8jAKLo9z*D!n_NZI-UBSH(&Mcfqh5@6%#v_vm8tpii3+yx^%pF%ytTj!Zb zxiI*$^POh~wK`Y}p$b$xRj9TrP~8HxUAJcY@m=p=O6&QdXjYL1MQ+@w!szRca|B__ z2M5LaqBNN0J|<#xp|985o9e4dwv?Lhl+YM$cqT2MC#JLw+O;UoIE5#obH*vv{kLI}o!(IIIq5tL|sNT2E@)H062f#|R literal 0 HcmV?d00001 diff --git a/tests/data/Reader/Gnumeric/apostrophe3b.gnumeric b/tests/data/Reader/Gnumeric/apostrophe3b.gnumeric new file mode 100644 index 0000000000000000000000000000000000000000..7517660442184eecf4a29b99a0704b24ab085590 GIT binary patch literal 1760 zcmV<61|Rt!iwFP!000001I<`%Q`EK3O42fBmf_+c*vZdUG>9m|?8d?rOFB?29=4ypE~$fJI6;>)6d&!?rLBc|_Q} zW8eC{gOB#-?wiv&i=8iA+)p{bw?KrY4v;$bLdwLc*H^1mZO)c479^~3F|R)~8?{JA zcE-C-$rkVGP;<2!jb{C~>%k;k;20ecmP*7z3{oj^QcWE25J_#@-lI-x>ol^vZ!Alj zd1o$Tc?k~}a3$NiMt^YuUoH3~Onn|5+g5K$sk(8qRfA={kawjNWV)0%RirA>&@w7R zG4AGWxg&VxIO&3~f&~}kJ7*HnAPohlbc)1jy^tuYctCWCyZ-prGYn>J4z53O^1rYK zqAN-G|FDH|i4`f3Xqx^MGa??KrQ|`#nb>ev#|F@|&ppMzsOhNcwvB^Fs3)%9>)5K^Et zfUh9CX9P|r&Zbb_TyxH%&t zmCvT#d$8n>+?4lk-?{IaJ4u)cBs?X`b6q!MDF~%h-M&I$t5$G{IRsQ}tS=;zs%16X z;O8q$<_p=e4-Z>aAE#B{S{EcuDBAS6hyeIz24g=(Dhn`?V{{e7HZ@KK9Ll^XyUJbFS-0w z3V|W~=SEl3qfn;3*=IArH>&6`p%inN9(QbzJh4U`L=}Epfa;OJvs*?0o^folYKwo$ zrR4DsiBLR~g}{f_i~K@m$BzgsbWZ+=#EipW@j8h-6DsaAl?9pzw&W&(R*+y3LgPVm z=$?p9Vah7{_NvFy9^o9k0O zr8o4(?g092Y46VLo!$8U&9!ZP5h(HTTIy3L3%Jg`&wIqI$6RL9q22^Y=r1*2F~b4R z@OvETrVgV5kTn&?4V*V@>kNhP9O%K1Q~ZI#gH&wUR&Q5w;6|_$Gnh5$)>R91K{A<& z)l|wkHKR)(9@WH*wzpuOP;-Yd8{R$l*oRbK&KHIf3X*8^yA#B+*QV0D<=J}S+GtS?G~S>8Gk{R;)sZf9rCFDrSZRD7p|257^V((-v?N}JoPL~+6? zyd14-9${*1)l&k8cUD}iVYu{34By9%;=!fcB`k-W4e?wj6HM8mnYZ1ggEE) zV*_N&)a3L4bM(06^wMjn@;G{YTHN+85WM(JTyW&0X{Nw%=l_%FzWE2Jo9~|q6aWA^ CHfP%a literal 0 HcmV?d00001 From 2250a0e53b88cbdd667fc93fc33dfc95766d09e7 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:57:53 -0800 Subject: [PATCH 6/9] Yet Another Test --- .../Reader/Xlsx/ApostropheTest.php | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/ApostropheTest.php diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ApostropheTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ApostropheTest.php new file mode 100644 index 000000000..3b759f8c6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ApostropheTest.php @@ -0,0 +1,61 @@ +setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('A2')->setValue(2); + $sheet->getCell('A3')->setValue(3); + $sheet->getCell('A4')->setValue(4); + $spreadsheet->addNamedRange(new NamedRange('sheet14cells', $sheet, '$A$1:$A$4')); + $sheet->getCell('C1') + ->setValue('=sheet14cells*sheet14cells'); + $sheet->getCell('E1')->setValue('=ANCHORARRAY(C1)'); + $sheet->getCell('G1')->setValue('=SINGLE(C1)'); + + $sheet1 = $spreadsheet->createSheet(); + $sheet1->setTitle("Apo'strophe"); + $sheet1->getCell('A1')->setValue(2); + $sheet1->getCell('A2')->setValue(3); + $sheet1->getCell('A3')->setValue(4); + $sheet1->getCell('A4')->setValue(5); + $spreadsheet->addNamedRange(new NamedRange('sheet24cells', $sheet1, '$A$1:$A$4')); + $sheet1->getCell('C1') + ->setValue('=sheet24cells*sheet24cells'); + $sheet1->getCell('E1')->setValue('=ANCHORARRAY(C1)'); + $sheet1->getCell('G1')->setValue('=SINGLE(C1)'); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + Calculation::getInstance($reloadedSpreadsheet) + ->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $rsheet = $reloadedSpreadsheet->getSheet(0); + self::assertSame([[1], [4], [9], [16]], $rsheet->getCell('C1')->getCalculatedValue()); + self::assertSame([[1], [4], [9], [16]], $rsheet->getCell('E1')->getCalculatedValue()); + self::assertSame(1, $rsheet->getCell('G1')->getCalculatedValue()); + + $rsheet1 = $reloadedSpreadsheet->getSheet(1); + self::assertSame([[4], [9], [16], [25]], $rsheet1->getCell('C1')->getCalculatedValue()); + self::assertSame([[4], [9], [16], [25]], $rsheet1->getCell('E1')->getCalculatedValue()); + self::assertSame(4, $rsheet1->getCell('G1')->getCalculatedValue()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} From 8e504039309d3722b18079fc57a558370dfbe167 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:43:27 -0800 Subject: [PATCH 7/9] More Tweaks --- src/PhpSpreadsheet/Reader/Gnumeric.php | 2 +- .../Reader/Xlsx/ApostropheTest.php | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index e1430072b..0a9274b03 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -517,7 +517,7 @@ class Gnumeric extends BaseReader continue; } - $value = str_replace("\\'", "'", $value); + $value = str_replace("\\'", "''", $value); [$worksheetName] = Worksheet::extractSheetTitle($value, true, true); $worksheet = $this->spreadsheet->getSheetByName($worksheetName); // Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ApostropheTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ApostropheTest.php index 3b759f8c6..dcb1d116c 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ApostropheTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ApostropheTest.php @@ -19,6 +19,7 @@ class ApostropheTest extends AbstractFunctional Calculation::RETURN_ARRAY_AS_ARRAY ); $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('sheet1'); $sheet->getCell('A1')->setValue(1); $sheet->getCell('A2')->setValue(2); $sheet->getCell('A3')->setValue(3); @@ -26,8 +27,8 @@ class ApostropheTest extends AbstractFunctional $spreadsheet->addNamedRange(new NamedRange('sheet14cells', $sheet, '$A$1:$A$4')); $sheet->getCell('C1') ->setValue('=sheet14cells*sheet14cells'); - $sheet->getCell('E1')->setValue('=ANCHORARRAY(C1)'); - $sheet->getCell('G1')->setValue('=SINGLE(C1)'); + $sheet->getCell('E1')->setValue('=ANCHORARRAY(sheet1!C1)'); + $sheet->getCell('G1')->setValue('=SINGLE(sheet1!C1:C4)'); $sheet1 = $spreadsheet->createSheet(); $sheet1->setTitle("Apo'strophe"); @@ -38,8 +39,8 @@ class ApostropheTest extends AbstractFunctional $spreadsheet->addNamedRange(new NamedRange('sheet24cells', $sheet1, '$A$1:$A$4')); $sheet1->getCell('C1') ->setValue('=sheet24cells*sheet24cells'); - $sheet1->getCell('E1')->setValue('=ANCHORARRAY(C1)'); - $sheet1->getCell('G1')->setValue('=SINGLE(C1)'); + $sheet1->getCell('E1')->setValue("=ANCHORARRAY('Apo''strophe'!C1)"); + $sheet1->getCell('G1')->setValue("=SINGLE('Apo''strophe'!C1:C4)"); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); $spreadsheet->disconnectWorksheets(); @@ -48,11 +49,17 @@ class ApostropheTest extends AbstractFunctional Calculation::RETURN_ARRAY_AS_ARRAY ); $rsheet = $reloadedSpreadsheet->getSheet(0); + // make sure results aren't from cache + $rsheet->getCell('E1')->setCalculatedValue(-1); + $rsheet->getCell('G1')->setCalculatedValue(-1); self::assertSame([[1], [4], [9], [16]], $rsheet->getCell('C1')->getCalculatedValue()); self::assertSame([[1], [4], [9], [16]], $rsheet->getCell('E1')->getCalculatedValue()); self::assertSame(1, $rsheet->getCell('G1')->getCalculatedValue()); $rsheet1 = $reloadedSpreadsheet->getSheet(1); + // make sure results aren't from cache + $rsheet1->getCell('E1')->setCalculatedValue(-1); + $rsheet1->getCell('G1')->setCalculatedValue(-1); self::assertSame([[4], [9], [16], [25]], $rsheet1->getCell('C1')->getCalculatedValue()); self::assertSame([[4], [9], [16], [25]], $rsheet1->getCell('E1')->getCalculatedValue()); self::assertSame(4, $rsheet1->getCell('G1')->getCalculatedValue()); From 771b3f1784915992859fd7b84f591438f1b66c2c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:35:42 -0800 Subject: [PATCH 8/9] Update OFFSET Fix #4376. --- src/PhpSpreadsheet/Calculation/LookupRef/Offset.php | 12 ++++++++---- .../Calculation/Functions/LookupRef/OffsetTest.php | 10 ++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php index c3643cd65..9d201ff22 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Worksheet\Validations; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Offset @@ -55,6 +56,10 @@ class Offset if (!is_object($cell)) { return ExcelError::REF(); } + $sheet = $cell->getParent()?->getParent(); // worksheet + if ($sheet !== null) { + $cellAddress = Validations::definedNameToCoordinate($cellAddress, $sheet); + } [$cellAddress, $worksheet] = self::extractWorksheet($cellAddress, $cell); @@ -62,12 +67,11 @@ class Offset if (strpos($cellAddress, ':')) { [$startCell, $endCell] = explode(':', $cellAddress); } - [$startCellColumn, $startCellRow] = Coordinate::coordinateFromString($startCell); - [$endCellColumn, $endCellRow] = Coordinate::coordinateFromString($endCell); + [$startCellColumn, $startCellRow] = Coordinate::indexesFromString($startCell); + [, $endCellRow, $endCellColumn] = Coordinate::indexesFromString($endCell); $startCellRow += $rows; - $startCellColumn = Coordinate::columnIndexFromString($startCellColumn) - 1; - $startCellColumn += $columns; + $startCellColumn += $columns - 1; if (($startCellRow <= 0) || ($startCellColumn < 0)) { return ExcelError::REF(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php index 5a6bb93ae..f1ad6919b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php @@ -74,4 +74,14 @@ class OffsetTest extends AllSetupTeardown self::assertSame(2, $workSheet->getCell('B2')->getCalculatedValue()); } + + public function testOffsetMultiCellNamedRange(): void + { + $sheet = $this->getSheet(); + $sheet->setCellValue('D13', 'Hello'); + $this->getSpreadsheet() + ->addNamedRange(new NamedRange('CELLAREA', $sheet, '$B$6:$F$22')); + $sheet->setCellValue('D1', '=OFFSET(CELLAREA,7,2,1,1)'); + self::assertSame('Hello', $sheet->getCell('D1')->getCalculatedValue()); + } } From 898ec448cdc578a1f1a63d66636295ace00bb54f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:28:47 -0800 Subject: [PATCH 9/9] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d60ca306..68d957ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Refactor Helper/Html. [PR #4359](https://github.com/PHPOffice/PhpSpreadsheet/pull/4359) +- Better handling of defined names on sheets whose titles include apostrophes. [Issue #4356](https://github.com/PHPOffice/PhpSpreadsheet/issues/4356) [Issue #4362](https://github.com/PHPOffice/PhpSpreadsheet/issues/4362) [Issue #4376](https://github.com/PHPOffice/PhpSpreadsheet/issues/4376) [PR #4360](https://github.com/PHPOffice/PhpSpreadsheet/pull/4360) ## 2025-02-08 - 4.0.0