Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct update to named ranges and formulae when inserting/deleting columns/rows #3077

Merged
merged 4 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).

### Fixed

- Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077)
- Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074)
- Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013)
- Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956)
Expand Down
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/DefinedName.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public function setName(string $name): self

// New title
$newTitle = $this->name;
ReferenceHelper::getInstance()->updateNamedFormulas($this->worksheet->getParent(), $oldTitle, $newTitle);
ReferenceHelper::getInstance()->updateNamedFormulae($this->worksheet->getParent(), $oldTitle, $newTitle);
}

return $this;
Expand Down
45 changes: 38 additions & 7 deletions src/PhpSpreadsheet/ReferenceHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -538,11 +538,7 @@ function ($coordinate) use ($cellCollection) {

// 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
Expand Down Expand Up @@ -866,13 +862,13 @@ private function updateCellReference($cellReference = 'A1', bool $includeAbsolut
}

/**
* Update named formulas (i.e. containing worksheet references / named ranges).
* Update named formulae (i.e. containing worksheet references / named ranges).
*
* @param Spreadsheet $spreadsheet Object to update
* @param string $oldName Old name (name to replace)
* @param string $newName New name
*/
public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void
public function updateNamedFormulae(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void
{
if ($oldName == '') {
return;
Expand All @@ -893,6 +889,41 @@ public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $ne
}
}

private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
{
foreach ($worksheet->getParent()->getDefinedNames() as $definedName) {
if ($definedName->isFormula() === false) {
$this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
} else {
$this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
}
}
}

private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
{
$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, '=')));
}
}
}

private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
{
if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) {
$formula = $definedName->getValue();
$formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle());
$definedName->setValue($formula);
}
}

/**
* Update cell range.
*
Expand Down
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Worksheet/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ public function setTitle($title, $updateFormulaCellReferences = true, $validate
$this->parent->getCalculationEngine()
->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
if ($updateFormulaCellReferences) {
ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle);
ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
}
}

Expand Down
109 changes: 109 additions & 0 deletions tests/PhpSpreadsheetTests/ReferenceHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -538,4 +541,110 @@ public function testDeleteColumnsWithPrintArea(): void
$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;
}
}