Skip to content

Commit a936254

Browse files
authored
Merge pull request PHPOffice#3528 from oleibman/pr1449
Partial Solution for Removing Rows or Columns that Include Edge Ranges
2 parents b4cd42d + 1e2c86b commit a936254

File tree

5 files changed

+163
-21
lines changed

5 files changed

+163
-21
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3535
- Refactor Helper/Html. [PR #4359](https://github.com/PHPOffice/PhpSpreadsheet/pull/4359)
3636
- Ignore ignoredErrors when not applicable. [Issue #4375](https://github.com/PHPOffice/PhpSpreadsheet/issues/4375) [PR #4377](https://github.com/PHPOffice/PhpSpreadsheet/pull/4377)
3737
- 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)
38+
- Partial solution for removing rows or columns that include edge ranges. [Issue #1449](https://github.com/PHPOffice/PhpSpreadsheet/issues/1449) [PR #3528](https://github.com/PHPOffice/PhpSpreadsheet/pull/3528)
3839

3940
## 2025-02-08 - 4.0.0
4041

src/PhpSpreadsheet/CellReferenceHelper.php

+63-4
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,30 @@ class CellReferenceHelper
1111

1212
protected int $beforeColumn;
1313

14+
protected bool $beforeColumnAbsolute = false;
15+
16+
protected string $beforeColumnString;
17+
1418
protected int $beforeRow;
1519

20+
protected bool $beforeRowAbsolute = false;
21+
1622
protected int $numberOfColumns;
1723

1824
protected int $numberOfRows;
1925

2026
public function __construct(string $beforeCellAddress = 'A1', int $numberOfColumns = 0, int $numberOfRows = 0)
2127
{
28+
$this->beforeColumnAbsolute = $beforeCellAddress[0] === '$';
29+
$this->beforeRowAbsolute = strpos($beforeCellAddress, '$', 1) !== false;
2230
$this->beforeCellAddress = str_replace('$', '', $beforeCellAddress);
2331
$this->numberOfColumns = $numberOfColumns;
2432
$this->numberOfRows = $numberOfRows;
2533

2634
// Get coordinate of $beforeCellAddress
2735
[$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress);
28-
$this->beforeColumn = Coordinate::columnIndexFromString($beforeColumn);
36+
$this->beforeColumnString = $beforeColumn;
37+
$this->beforeColumn = (int) Coordinate::columnIndexFromString($beforeColumn);
2938
$this->beforeRow = (int) $beforeRow;
3039
}
3140

@@ -41,7 +50,7 @@ public function refreshRequired(string $beforeCellAddress, int $numberOfColumns,
4150
|| $this->numberOfRows !== $numberOfRows;
4251
}
4352

44-
public function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false): string
53+
public function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false, ?bool $topLeft = null): string
4554
{
4655
if (Coordinate::coordinateIsRange($cellReference)) {
4756
throw new Exception('Only single cell references may be passed to this method.');
@@ -62,8 +71,13 @@ public function updateCellReference(string $cellReference = 'A1', bool $includeA
6271
$updateColumn = (($absoluteColumn !== '$') && $newColumnIndex >= $this->beforeColumn);
6372
$updateRow = (($absoluteRow !== '$') && $newRowIndex >= $this->beforeRow);
6473
} else {
65-
$updateColumn = ($newColumnIndex >= $this->beforeColumn);
66-
$updateRow = ($newRowIndex >= $this->beforeRow);
74+
$newColumnIndex = $this->computeNewColumnIndex($newColumnIndex, $topLeft);
75+
$newColumn = $absoluteColumn . Coordinate::stringFromColumnIndex($newColumnIndex);
76+
$updateColumn = false;
77+
78+
$newRowIndex = $this->computeNewRowIndex($newRowIndex, $topLeft);
79+
$newRow = $absoluteRow . $newRowIndex;
80+
$updateRow = false;
6781
}
6882

6983
// Create new column reference
@@ -80,6 +94,51 @@ public function updateCellReference(string $cellReference = 'A1', bool $includeA
8094
return "{$newColumn}{$newRow}";
8195
}
8296

97+
public function computeNewColumnIndex(int $newColumnIndex, ?bool $topLeft): int
98+
{
99+
// A special case is removing the left/top or bottom/right edge of a range
100+
// $topLeft is null if we aren't adjusting a range at all.
101+
if (
102+
$topLeft !== null
103+
&& $this->numberOfColumns < 0
104+
&& $newColumnIndex >= $this->beforeColumn + $this->numberOfColumns
105+
&& $newColumnIndex <= $this->beforeColumn - 1
106+
) {
107+
if ($topLeft) {
108+
$newColumnIndex = $this->beforeColumn + $this->numberOfColumns;
109+
} else {
110+
$newColumnIndex = $this->beforeColumn + $this->numberOfColumns - 1;
111+
}
112+
} elseif ($newColumnIndex >= $this->beforeColumn) {
113+
// Create new column reference
114+
$newColumnIndex += $this->numberOfColumns;
115+
}
116+
117+
return $newColumnIndex;
118+
}
119+
120+
public function computeNewRowIndex(int $newRowIndex, ?bool $topLeft): int
121+
{
122+
// A special case is removing the left/top or bottom/right edge of a range
123+
// $topLeft is null if we aren't adjusting a range at all.
124+
if (
125+
$topLeft !== null
126+
&& $this->numberOfRows < 0
127+
&& $newRowIndex >= $this->beforeRow + $this->numberOfRows
128+
&& $newRowIndex <= $this->beforeRow - 1
129+
) {
130+
if ($topLeft) {
131+
$newRowIndex = $this->beforeRow + $this->numberOfRows;
132+
} else {
133+
$newRowIndex = $this->beforeRow + $this->numberOfRows - 1;
134+
}
135+
} elseif ($newRowIndex >= $this->beforeRow) {
136+
$newRowIndex = $newRowIndex + $this->numberOfRows;
137+
}
138+
139+
return $newRowIndex;
140+
}
141+
83142
public function cellAddressInDeleteRange(string $cellAddress): bool
84143
{
85144
[$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress);

src/PhpSpreadsheet/ReferenceHelper.php

+26-16
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,10 @@ protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns,
194194
if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
195195
$worksheet->setHyperlink($cellAddress, null);
196196
} elseif ($cellAddress !== $newReference) {
197-
$worksheet->setHyperlink($newReference, $value);
198197
$worksheet->setHyperlink($cellAddress, null);
198+
if ($newReference) {
199+
$worksheet->setHyperlink($newReference, $value);
200+
}
199201
}
200202
}
201203
}
@@ -292,6 +294,9 @@ protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColu
292294
if ($cellAddress !== $newReference) {
293295
$worksheet->setDataValidation($newReference, $dataValidation);
294296
$worksheet->setDataValidation($cellAddress, null);
297+
if ($newReference) {
298+
$worksheet->setDataValidation($newReference, $dataValidation);
299+
}
295300
}
296301
}
297302
}
@@ -307,7 +312,9 @@ protected function adjustMergeCells(Worksheet $worksheet): void
307312
$aNewMergeCells = []; // the new array of all merge cells
308313
foreach ($aMergeCells as $cellAddress => &$value) {
309314
$newReference = $this->updateCellReference($cellAddress);
310-
$aNewMergeCells[$newReference] = $newReference;
315+
if ($newReference) {
316+
$aNewMergeCells[$newReference] = $newReference;
317+
}
311318
}
312319
$worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array
313320
}
@@ -328,8 +335,10 @@ protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColum
328335
foreach ($aProtectedCells as $cellAddress => $protectedRange) {
329336
$newReference = $this->updateCellReference($cellAddress);
330337
if ($cellAddress !== $newReference) {
331-
$worksheet->protectCells($newReference, $protectedRange->getPassword(), true);
332338
$worksheet->unprotectCells($cellAddress);
339+
if ($newReference) {
340+
$worksheet->protectCells($newReference, $protectedRange->getPassword(), true);
341+
}
333342
}
334343
}
335344
}
@@ -457,7 +466,8 @@ public function insertNewBefore(
457466
$cell = $worksheet->getCell($coordinate);
458467
$cellIndex = Coordinate::columnIndexFromString($cell->getColumn());
459468

460-
if ($cellIndex - 1 + $numberOfColumns < 0) {
469+
// Don't update cells that are being removed
470+
if ($numberOfColumns < 0 && $cellIndex >= $beforeColumn + $numberOfColumns && $cellIndex < $beforeColumn) {
461471
continue;
462472
}
463473

@@ -633,8 +643,8 @@ public function updateFormulaReferences(
633643
if ($matchCount > 0) {
634644
foreach ($matches as $match) {
635645
$fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
636-
$modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences), 2);
637-
$modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences), 2);
646+
$modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 2);
647+
$modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 2);
638648

639649
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
640650
if (self::matchSheetName($match[2], $worksheetName)) {
@@ -657,8 +667,8 @@ public function updateFormulaReferences(
657667
if ($matchCount > 0) {
658668
foreach ($matches as $match) {
659669
$fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
660-
$modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences), 0, -2);
661-
$modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences), 0, -2);
670+
$modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 0, -2);
671+
$modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 0, -2);
662672

663673
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
664674
if (self::matchSheetName($match[2], $worksheetName)) {
@@ -681,8 +691,8 @@ public function updateFormulaReferences(
681691
if ($matchCount > 0) {
682692
foreach ($matches as $match) {
683693
$fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
684-
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences);
685-
$modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences);
694+
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true);
695+
$modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false);
686696

687697
if ($match[3] . $match[4] !== $modified3 . $modified4) {
688698
if (self::matchSheetName($match[2], $worksheetName)) {
@@ -709,7 +719,7 @@ public function updateFormulaReferences(
709719
foreach ($matches as $match) {
710720
$fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}");
711721

712-
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences);
722+
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, null);
713723
if ($match[3] !== $modified3) {
714724
if (self::matchSheetName($match[2], $worksheetName)) {
715725
$toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3");
@@ -890,7 +900,7 @@ private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows
890900
*
891901
* @return string Updated cell range
892902
*/
893-
private function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false): string
903+
private function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false, ?bool $topLeft = null)
894904
{
895905
// Is it in another worksheet? Will not have to update anything.
896906
if (str_contains($cellReference, '!')) {
@@ -902,7 +912,7 @@ private function updateCellReference(string $cellReference = 'A1', bool $include
902912
/** @var CellReferenceHelper */
903913
$cellReferenceHelper = $this->cellReferenceHelper;
904914

905-
return $cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences);
915+
return $cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences, $topLeft);
906916
}
907917

908918
// Range
@@ -1008,14 +1018,14 @@ private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsol
10081018
$cellReferenceHelper = $this->cellReferenceHelper;
10091019
if (ctype_alpha($range[$i][$j])) {
10101020
$range[$i][$j] = Coordinate::coordinateFromString(
1011-
$cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences, $onlyAbsoluteReferences)
1021+
$cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences, $onlyAbsoluteReferences, null)
10121022
)[0];
10131023
} elseif (ctype_digit($range[$i][$j])) {
10141024
$range[$i][$j] = Coordinate::coordinateFromString(
1015-
$cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences)
1025+
$cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null)
10161026
)[1];
10171027
} else {
1018-
$range[$i][$j] = $cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences);
1028+
$range[$i][$j] = $cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null);
10191029
}
10201030
}
10211031
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests;
4+
5+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class Issue1449Test extends TestCase
9+
{
10+
/** @var bool */
11+
private $skipTests = true;
12+
13+
public function testDeleteColumns(): void
14+
{
15+
$spreadsheet = new Spreadsheet();
16+
$sheet1 = $spreadsheet->getActiveSheet();
17+
$sheet2 = $spreadsheet->createSheet();
18+
$sheet1->setTitle('Sheet1');
19+
$sheet2->setTitle('Sheet2');
20+
$sheet1->fromArray(
21+
[
22+
[3, 1, 2, 33, 1, 10, 20, 30, 40],
23+
[4, 2, 3, 23, 2, 10, 20, 30, 40],
24+
[5, 3, 4, 1, 3, 10, 20, 30, 40],
25+
[6, 4, 6, 4, 3, 10, 20, 30, 40],
26+
[7, 6, 6, 2, 2, 10, 20, 30, 40],
27+
],
28+
null,
29+
'C1',
30+
true
31+
);
32+
$sheet1->getCell('A1')->setValue('=SUM(C4:F7)');
33+
$sheet2->getCell('A1')->setValue('=SUM(Sheet1!C3:G5)');
34+
$sheet1->removeColumn('F', 4);
35+
self::assertSame('=SUM(C4:E7)', $sheet1->getCell('A1')->getValue());
36+
if (!$this->skipTests) {
37+
// References on another sheet not working yet.
38+
self::assertSame('=Sheet1!SUM(C3:E5)', $sheet2->getCell('A1')->getValue());
39+
}
40+
$spreadsheet->disconnectWorksheets();
41+
}
42+
43+
public function testDeleteRows(): void
44+
{
45+
$spreadsheet = new Spreadsheet();
46+
$sheet1 = $spreadsheet->getActiveSheet();
47+
$sheet2 = $spreadsheet->createSheet();
48+
$sheet1->setTitle('Sheet1');
49+
$sheet2->setTitle('Sheet2');
50+
$sheet1->fromArray(
51+
[
52+
[3, 1, 2, 33, 1, 10, 20, 30, 40],
53+
[4, 2, 3, 23, 2, 10, 20, 30, 40],
54+
[5, 3, 4, 1, 3, 10, 20, 30, 40],
55+
[6, 4, 6, 4, 3, 10, 20, 30, 40],
56+
[7, 6, 6, 2, 2, 10, 20, 30, 40],
57+
],
58+
null,
59+
'C1',
60+
true
61+
);
62+
$sheet1->getCell('A1')->setValue('=SUM(C4:F7)');
63+
$sheet2->getCell('A1')->setValue('=SUM(Sheet1!C3:G5)');
64+
$sheet1->removeRow(4, 2);
65+
self::assertSame('=SUM(C4:F5)', $sheet1->getCell('A1')->getValue());
66+
if (!$this->skipTests) {
67+
// References on another sheet not working yet.
68+
self::assertSame('=Sheet1!SUM(C3:G3)', $sheet2->getCell('A1')->getValue());
69+
}
70+
$spreadsheet->disconnectWorksheets();
71+
}
72+
}

tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public function testCanInsertImageGoodProtocol(): void
1616
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
1717
self::markTestSkipped('Skipped due to setting of environment variable');
1818
}
19-
$imagePath = 'https://phpspreadsheet.readthedocs.io/en/latest/topics/images/01-03-filter-icon-1.png';
19+
$imagePath = 'https://phpspreadsheet.readthedocs.io/en/stable/topics/images/01-03-filter-icon-1.png';
2020
$html = '<table>
2121
<tr>
2222
<td><img src="' . $imagePath . '" alt="test image voilà"></td>

0 commit comments

Comments
 (0)