Skip to content

Commit 99d4955

Browse files
committed
WIP Removing Rows or Columns that Include Edge Ranges
This PR will probably remain in draft status for some time. It needs updates, and I need more time to understand exactly what it is doing. It is a replacement for PR PHPOffice#2096, intended as a fix for issue PHPOffice#1449. That PR has been stuck, with a good deal of acrimony, for over 2 years. In the interim, all the parameters for the functions involved were renamed, the functions were refactored to have fewer parameters with new class properties replacing the missing parameters, special code was added for absolute references, and code was refactored putting some of the methods involved in a new class under a new method name. In addition, the original PR was not Php8-compliant (one of the sticking points regarding implementing it), and there were a great many formatting problems (another sticking point). This has made it very difficult to figure out exactly what was intended in the original PR. This PR (a) addresses some of the issues associated with the original issue (deleting columns/rows at the end of a range passed to a formula *on the same sheet*), and (b) does not cause any existing tests to fail. Many more unit tests are needed (another sticking point); in particular, there are changes involving data validations, comments, defined names, and other elements which are not adequately tested. Aside from those, PhpSpreadsheet does not adjust formulas on a different sheet which involve deleted rows/columns, and it should, as Excel does. Similarly, it appears that Excel doesn't do anything extraordinary when the right/bottom part of the range is deleted, but when the left/top part is deleted, it converts the range to #REF! at least some of the time. This needs more research; if confirmed, it needs to be added to PhpSpreadsheet.
1 parent aab1614 commit 99d4955

File tree

3 files changed

+168
-37
lines changed

3 files changed

+168
-37
lines changed

src/PhpSpreadsheet/CellReferenceHelper.php

+53-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ class CellReferenceHelper
1717
*/
1818
protected $beforeColumn;
1919

20+
/** @var string */
21+
protected $beforeColumnString;
22+
23+
/** @var bool */
24+
protected $beforeColumnAbsolute = false;
25+
26+
/** @var bool */
27+
protected $beforeRowAbsolute = false;
28+
2029
/**
2130
* @var int
2231
*/
@@ -34,12 +43,15 @@ class CellReferenceHelper
3443

3544
public function __construct(string $beforeCellAddress = 'A1', int $numberOfColumns = 0, int $numberOfRows = 0)
3645
{
46+
$this->beforeColumnAbsolute = $beforeCellAddress[0] === '$';
47+
$this->beforeRowAbsolute = strpos($beforeCellAddress, '$', 1) !== false;
3748
$this->beforeCellAddress = str_replace('$', '', $beforeCellAddress);
3849
$this->numberOfColumns = $numberOfColumns;
3950
$this->numberOfRows = $numberOfRows;
4051

4152
// Get coordinate of $beforeCellAddress
4253
[$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress);
54+
$this->beforeColumnString = $beforeColumn;
4355
$this->beforeColumn = (int) Coordinate::columnIndexFromString($beforeColumn);
4456
$this->beforeRow = (int) $beforeRow;
4557
}
@@ -56,7 +68,7 @@ public function refreshRequired(string $beforeCellAddress, int $numberOfColumns,
5668
$this->numberOfRows !== $numberOfRows;
5769
}
5870

59-
public function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false): string
71+
public function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, ?bool $topLeft = null): string
6072
{
6173
if (Coordinate::coordinateIsRange($cellReference)) {
6274
throw new Exception('Only single cell references may be passed to this method.');
@@ -74,8 +86,46 @@ public function updateCellReference(string $cellReference = 'A1', bool $includeA
7486
$updateColumn = (($absoluteColumn !== '$') && $newColumnIndex >= $this->beforeColumn);
7587
$updateRow = (($absoluteRow !== '$') && $newRowIndex >= $this->beforeRow);
7688
} else {
77-
$updateColumn = ($newColumnIndex >= $this->beforeColumn);
78-
$updateRow = ($newRowIndex >= $this->beforeRow);
89+
// A special case is removing the left/top or bottom/right edge of a range
90+
// $topLeft is null if we aren't adjusting a range at all.
91+
if (
92+
$topLeft !== null
93+
&& $this->numberOfColumns < 0
94+
&& $newColumnIndex >= $this->beforeColumn + $this->numberOfColumns
95+
&& $newColumnIndex <= $this->beforeColumn - 1
96+
) {
97+
if ($topLeft) {
98+
$newColumnIndex = $this->beforeColumn + $this->numberOfColumns;
99+
$newColumn = Coordinate::stringFromColumnIndex($newColumnIndex);
100+
} else {
101+
$newColumnIndex = $this->beforeColumn + $this->numberOfColumns - 1;
102+
}
103+
} elseif ($newColumnIndex >= $this->beforeColumn) {
104+
// Create new column reference
105+
$newColumnIndex += $this->numberOfColumns;
106+
}
107+
$newColumn = $absoluteColumn . Coordinate::stringFromColumnIndex($newColumnIndex);
108+
//$updateColumn = ($newColumnIndex >= $this->beforeColumn);
109+
$updateColumn = false;
110+
// A special case is removing the left/top or bottom/right edge of a range
111+
// $topLeft is null if we aren't adjusting a range at all.
112+
if (
113+
$topLeft !== null
114+
&& $this->numberOfRows < 0
115+
&& $newRowIndex >= $this->beforeRow + $this->numberOfRows
116+
&& $newRowIndex <= $this->beforeRow - 1
117+
) {
118+
if ($topLeft) {
119+
$newRowIndex = $this->beforeRow + $this->numberOfRows;
120+
} else {
121+
$newRowIndex = $this->beforeRow + $this->numberOfRows - 1;
122+
}
123+
} elseif ($newRowIndex >= $this->beforeRow) {
124+
$newRowIndex = $newRowIndex + $this->numberOfRows;
125+
}
126+
$newRow = $absoluteRow . $newRowIndex;
127+
//$updateRow = ($newRowIndex >= $this->beforeRow);
128+
$updateRow = false;
79129
}
80130

81131
// Create new column reference

src/PhpSpreadsheet/ReferenceHelper.php

+43-34
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ protected function adjustPageBreaks(Worksheet $worksheet, int $numberOfColumns,
154154
} else {
155155
// Otherwise update any affected breaks by inserting a new break at the appropriate point
156156
// and removing the old affected break
157-
$newReference = $this->updateCellReference($cellAddress);
157+
$newReference = $this->updateCellReference($cellAddress, false, null);
158158
if ($cellAddress !== $newReference) {
159159
$worksheet->setBreak($newReference, $value)
160160
->setBreak($cellAddress, Worksheet::BREAK_NONE);
@@ -177,7 +177,7 @@ protected function adjustComments(Worksheet $worksheet): void
177177
// Any comments inside a deleted range will be ignored
178178
if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) {
179179
// Otherwise build a new array of comments indexed by the adjusted cell reference
180-
$newReference = $this->updateCellReference($cellAddress);
180+
$newReference = $this->updateCellReference($cellAddress, false, null);
181181
$aNewComments[$newReference] = $value;
182182
}
183183
}
@@ -200,12 +200,14 @@ protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns,
200200
: uksort($aHyperlinkCollection, [self::class, 'cellSort']);
201201

202202
foreach ($aHyperlinkCollection as $cellAddress => $value) {
203-
$newReference = $this->updateCellReference($cellAddress);
203+
$newReference = $this->updateCellReference($cellAddress, false, null);
204204
if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
205205
$worksheet->setHyperlink($cellAddress, null);
206206
} elseif ($cellAddress !== $newReference) {
207-
$worksheet->setHyperlink($newReference, $value);
208207
$worksheet->setHyperlink($cellAddress, null);
208+
if ($newReference) {
209+
$worksheet->setHyperlink($newReference, $value);
210+
}
209211
}
210212
}
211213
}
@@ -226,7 +228,7 @@ protected function adjustConditionalFormatting(Worksheet $worksheet, int $number
226228

227229
foreach ($aStyles as $cellAddress => $cfRules) {
228230
$worksheet->removeConditionalStyles($cellAddress);
229-
$newReference = $this->updateCellReference($cellAddress);
231+
$newReference = $this->updateCellReference($cellAddress, false, null);
230232

231233
foreach ($cfRules as &$cfRule) {
232234
/** @var Conditional $cfRule */
@@ -264,11 +266,13 @@ protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColu
264266
: uksort($aDataValidationCollection, [self::class, 'cellSort']);
265267

266268
foreach ($aDataValidationCollection as $cellAddress => $dataValidation) {
267-
$newReference = $this->updateCellReference($cellAddress);
269+
$newReference = $this->updateCellReference($cellAddress, false, null);
268270
if ($cellAddress !== $newReference) {
269271
$dataValidation->setSqref($newReference);
270-
$worksheet->setDataValidation($newReference, $dataValidation);
271272
$worksheet->setDataValidation($cellAddress, null);
273+
if ($newReference) {
274+
$worksheet->setDataValidation($newReference, $dataValidation);
275+
}
272276
}
273277
}
274278
}
@@ -283,8 +287,10 @@ protected function adjustMergeCells(Worksheet $worksheet): void
283287
$aMergeCells = $worksheet->getMergeCells();
284288
$aNewMergeCells = []; // the new array of all merge cells
285289
foreach ($aMergeCells as $cellAddress => &$value) {
286-
$newReference = $this->updateCellReference($cellAddress);
287-
$aNewMergeCells[$newReference] = $newReference;
290+
$newReference = $this->updateCellReference($cellAddress, false, null);
291+
if ($newReference) {
292+
$aNewMergeCells[$newReference] = $newReference;
293+
}
288294
}
289295
$worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array
290296
}
@@ -303,10 +309,12 @@ protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColum
303309
? uksort($aProtectedCells, [self::class, 'cellReverseSort'])
304310
: uksort($aProtectedCells, [self::class, 'cellSort']);
305311
foreach ($aProtectedCells as $cellAddress => $value) {
306-
$newReference = $this->updateCellReference($cellAddress);
312+
$newReference = $this->updateCellReference($cellAddress, false, null);
307313
if ($cellAddress !== $newReference) {
308-
$worksheet->protectCells($newReference, $value, true);
309314
$worksheet->unprotectCells($cellAddress);
315+
if ($newReference) {
316+
$worksheet->protectCells($newReference, $value, true);
317+
}
310318
}
311319
}
312320
}
@@ -321,7 +329,7 @@ protected function adjustColumnDimensions(Worksheet $worksheet): void
321329
$aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true);
322330
if (!empty($aColumnDimensions)) {
323331
foreach ($aColumnDimensions as $objColumnDimension) {
324-
$newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1');
332+
$newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1', false, null);
325333
[$newReference] = Coordinate::coordinateFromString($newReference);
326334
if ($objColumnDimension->getColumnIndex() !== $newReference) {
327335
$objColumnDimension->setColumnIndex($newReference);
@@ -344,7 +352,7 @@ protected function adjustRowDimensions(Worksheet $worksheet, $beforeRow, $number
344352
$aRowDimensions = array_reverse($worksheet->getRowDimensions(), true);
345353
if (!empty($aRowDimensions)) {
346354
foreach ($aRowDimensions as $objRowDimension) {
347-
$newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex());
355+
$newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex(), false, null);
348356
[, $newReference] = Coordinate::coordinateFromString($newReference);
349357
$newRoweference = (int) $newReference;
350358
if ($objRowDimension->getRowIndex() !== $newRoweference) {
@@ -434,7 +442,8 @@ function ($coordinate) use ($cellCollection) {
434442
$cell = $worksheet->getCell($coordinate);
435443
$cellIndex = Coordinate::columnIndexFromString($cell->getColumn());
436444

437-
if ($cellIndex - 1 + $numberOfColumns < 0) {
445+
// Don't update cells that are being removed
446+
if ($numberOfColumns < 0 && $cellIndex >= $beforeColumn + $numberOfColumns && $cellIndex < $beforeColumn) {
438447
continue;
439448
}
440449

@@ -518,28 +527,28 @@ function ($coordinate) use ($cellCollection) {
518527
$splitCell = $worksheet->getFreezePane();
519528
$topLeftCell = $worksheet->getTopLeftCell() ?? '';
520529

521-
$splitCell = $this->updateCellReference($splitCell);
522-
$topLeftCell = $this->updateCellReference($topLeftCell);
530+
$splitCell = $this->updateCellReference($splitCell, false, null);
531+
$topLeftCell = $this->updateCellReference($topLeftCell, false, null);
523532

524533
$worksheet->freezePane($splitCell, $topLeftCell);
525534
}
526535

527536
// Page setup
528537
if ($worksheet->getPageSetup()->isPrintAreaSet()) {
529538
$worksheet->getPageSetup()->setPrintArea(
530-
$this->updateCellReference($worksheet->getPageSetup()->getPrintArea())
539+
$this->updateCellReference($worksheet->getPageSetup()->getPrintArea(), false, null)
531540
);
532541
}
533542

534543
// Update worksheet: drawings
535544
$aDrawings = $worksheet->getDrawingCollection();
536545
foreach ($aDrawings as $objDrawing) {
537-
$newReference = $this->updateCellReference($objDrawing->getCoordinates());
546+
$newReference = $this->updateCellReference($objDrawing->getCoordinates(), false, null);
538547
if ($objDrawing->getCoordinates() != $newReference) {
539548
$objDrawing->setCoordinates($newReference);
540549
}
541550
if ($objDrawing->getCoordinates2() !== '') {
542-
$newReference = $this->updateCellReference($objDrawing->getCoordinates2());
551+
$newReference = $this->updateCellReference($objDrawing->getCoordinates2(), false, null);
543552
if ($objDrawing->getCoordinates2() != $newReference) {
544553
$objDrawing->setCoordinates2($newReference);
545554
}
@@ -596,8 +605,8 @@ public function updateFormulaReferences(
596605
foreach ($matches as $match) {
597606
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
598607
$fromString .= $match[3] . ':' . $match[4];
599-
$modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences), 2);
600-
$modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences), 2);
608+
$modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences, true), 2);
609+
$modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences, false), 2);
601610

602611
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
603612
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -621,8 +630,8 @@ public function updateFormulaReferences(
621630
foreach ($matches as $match) {
622631
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
623632
$fromString .= $match[3] . ':' . $match[4];
624-
$modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences), 0, -2);
625-
$modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences), 0, -2);
633+
$modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences, true), 0, -2);
634+
$modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences, false), 0, -2);
626635

627636
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
628637
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -646,8 +655,8 @@ public function updateFormulaReferences(
646655
foreach ($matches as $match) {
647656
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
648657
$fromString .= $match[3] . ':' . $match[4];
649-
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences);
650-
$modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences);
658+
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, true);
659+
$modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences, false);
651660

652661
if ($match[3] . $match[4] !== $modified3 . $modified4) {
653662
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -674,7 +683,7 @@ public function updateFormulaReferences(
674683
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
675684
$fromString .= $match[3];
676685

677-
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences);
686+
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, null);
678687
if ($match[3] !== $modified3) {
679688
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
680689
$toString = ($match[2] > '') ? $match[2] . '!' : '';
@@ -854,7 +863,7 @@ private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows
854863
*
855864
* @return string Updated cell range
856865
*/
857-
private function updateCellReference($cellReference = 'A1', bool $includeAbsoluteReferences = false)
866+
private function updateCellReference($cellReference = 'A1', bool $includeAbsoluteReferences = false, ?bool $topLeft = null)
858867
{
859868
// Is it in another worksheet? Will not have to update anything.
860869
if (strpos($cellReference, '!') !== false) {
@@ -863,7 +872,7 @@ private function updateCellReference($cellReference = 'A1', bool $includeAbsolut
863872
// Is it a range or a single cell?
864873
if (!Coordinate::coordinateIsRange($cellReference)) {
865874
// Single cell
866-
return $this->cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences);
875+
return $this->cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences, $topLeft);
867876
}
868877

869878
// Range
@@ -925,7 +934,7 @@ private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet
925934
$formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true);
926935
$definedName->setValue($formula);
927936
} else {
928-
$definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true));
937+
$definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true, null));
929938
}
930939
}
931940
}
@@ -967,14 +976,14 @@ private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsol
967976
for ($j = 0; $j < $jc; ++$j) {
968977
if (ctype_alpha($range[$i][$j])) {
969978
$range[$i][$j] = Coordinate::coordinateFromString(
970-
$this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences)
979+
$this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences, null)
971980
)[0];
972981
} elseif (ctype_digit($range[$i][$j])) {
973982
$range[$i][$j] = Coordinate::coordinateFromString(
974-
$this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences)
983+
$this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences, null)
975984
)[1];
976985
} else {
977-
$range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences);
986+
$range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences, null);
978987
}
979988
}
980989
}
@@ -1052,7 +1061,7 @@ private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddres
10521061
}
10531062

10541063
$worksheet->setAutoFilter(
1055-
$this->updateCellReference($autoFilterRange)
1064+
$this->updateCellReference($autoFilterRange, false, null)
10561065
);
10571066
}
10581067
}
@@ -1131,7 +1140,7 @@ private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, in
11311140
}
11321141
}
11331142

1134-
$table->setRange($this->updateCellReference($tableRange));
1143+
$table->setRange($this->updateCellReference($tableRange, false, null));
11351144
}
11361145
}
11371146
}

0 commit comments

Comments
 (0)