From abcbac6f9a9e8dba989cecb40e294231a7ed4b06 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 22 Sep 2022 01:10:53 +0200 Subject: [PATCH] Correct update to named ranges and formulae when inserting/deleting columns/rows --- src/PhpSpreadsheet/ReferenceHelper.php | 31 ++++- .../ReferenceHelperTest.php | 109 ++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 822b754a..16323ed5 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -538,11 +538,7 @@ class ReferenceHelper // Update workbook: define names if (count($worksheet->getParent()->getDefinedNames()) > 0) { - foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { - $definedName->setValue($this->updateCellReference($definedName->getValue())); - } - } + $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); } // Garbage collect @@ -1163,4 +1159,29 @@ class ReferenceHelper { throw new Exception('Cloning a Singleton is not allowed!'); } + + public function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { + if ($definedName->isFormula() === false) { + $cellAddress = $definedName->getValue(); + $asFormula = ($cellAddress[0] === '='); + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + if ($asFormula === true) { + $formula = $definedName->getValue(); + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } else { + $definedName->setValue($asFormula . $this->updateCellReference(ltrim($cellAddress, '='))); + } + } + } else { + $formula = $definedName->getValue(); + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } + } + } + } } diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index fbd60155..ac686a29 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -2,9 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; use PhpOffice\PhpSpreadsheet\Comment; +use PhpOffice\PhpSpreadsheet\NamedFormula; +use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard; @@ -538,4 +541,110 @@ class ReferenceHelperTest extends TestCase $printArea = $sheet->getPageSetup()->getPrintArea(); self::assertSame('A1:H10', $printArea); } + + public function testInsertRowsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->insertNewRowBefore(4, 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A8', $firstColumn->getRange()); + self::assertSame('=Data!B$2:B8', $secondColumn->getRange()); + self::assertSame(30, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(25, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(750, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + public function testInsertColumnsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->insertNewColumnBefore('B', 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A6', $firstColumn->getRange()); + self::assertSame('=Data!D$2:D6', $secondColumn->getRange()); + self::assertSame(30, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(25, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(750, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + public function testDeleteRowsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->removeRow(3, 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A4', $firstColumn->getRange()); + self::assertSame('=Data!B$2:B4', $secondColumn->getRange()); + self::assertSame(20, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(17, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(340, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + private function buildDefinedNamesTestWorkbook(): Spreadsheet + { + $spreadsheet = new Spreadsheet(); + $dataSheet = $spreadsheet->getActiveSheet(); + $dataSheet->setTitle('Data'); + + $totalsSheet = $spreadsheet->addSheet(new Worksheet()); + $totalsSheet->setTitle('Totals'); + + $spreadsheet->setActiveSheetIndexByName('Data'); + + $dataSheet->fromArray([['Column 1', 'Column 2'], [2, 1], [4, 3], [6, 5], [8, 7], [10, 9]], null, 'A1', true); + + $spreadsheet->addNamedRange( + new NamedRange('FirstColumn', $spreadsheet->getActiveSheet(), '=Data!$A$2:$A6') + ); + $spreadsheet->addNamedFormula( + new NamedFormula('FirstTotal', $spreadsheet->getActiveSheet(), '=SUM(FirstColumn)') + ); + $totalsSheet->setCellValue('A20', '=FirstTotal'); + + $spreadsheet->addNamedRange( + new NamedRange('SecondColumn', $spreadsheet->getActiveSheet(), '=Data!B$2:B6') + ); + $spreadsheet->addNamedFormula( + new NamedFormula('SecondTotal', $spreadsheet->getActiveSheet(), '=SUM(SecondColumn)') + ); + $totalsSheet->setCellValue('B20', '=SecondTotal'); + + $spreadsheet->addNamedFormula( + new NamedFormula('ProductTotal', $spreadsheet->getActiveSheet(), '=FirstTotal*SecondTotal') + ); + $totalsSheet->setCellValue('D20', '=ProductTotal'); + + return $spreadsheet; + } }