Skip to content

Commit 119c22a

Browse files
committed
Removing Columns/Rows Containing Merged Cells
Backport PR PHPOffice#4465.
1 parent 4aa3315 commit 119c22a

File tree

4 files changed

+249
-5
lines changed

4 files changed

+249
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1818
### Fixed
1919

2020
- TEXT and TIMEVALUE functions. [Issue #4249](https://github.com/PHPOffice/PhpSpreadsheet/issues/4249) [PR #4355](https://github.com/PHPOffice/PhpSpreadsheet/pull/4355)
21+
- Removing Columns/Rows Containing Merged Cells. Backport of [PR #4465](https://github.com/PHPOffice/PhpSpreadsheet/pull/4465)
2122

2223
## 2025-02-07 - 3.9.1
2324

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheet\Worksheet;
44

55
use ArrayObject;
6+
use Composer\Pcre\Preg;
67
use Generator;
78
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
89
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
@@ -1204,8 +1205,8 @@ private function getWorksheetAndCoordinate(string $coordinate): array
12041205
throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
12051206
}
12061207
} elseif (
1207-
!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
1208-
&& preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
1208+
!Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
1209+
&& Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
12091210
) {
12101211
// Named range?
12111212
$namedRange = $this->validateNamedRange($coordinate, true);
@@ -1727,7 +1728,7 @@ public function mergeCells(AddressRange|string|array $range, string $behaviour =
17271728
$range .= ":{$range}";
17281729
}
17291730

1730-
if (preg_match('/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/', $range, $matches) !== 1) {
1731+
if (!Preg::isMatch('/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/', $range, $matches)) {
17311732
throw new Exception('Merge must be on a valid range of cells.');
17321733
}
17331734

@@ -2363,6 +2364,42 @@ public function removeRow(int $row, int $numberOfRows = 1): static
23632364
if ($row < 1) {
23642365
throw new Exception('Rows to be deleted should at least start from row 1.');
23652366
}
2367+
$startRow = $row;
2368+
$endRow = $startRow + $numberOfRows - 1;
2369+
$removeKeys = [];
2370+
$addKeys = [];
2371+
foreach ($this->mergeCells as $key => $value) {
2372+
if (
2373+
Preg::isMatch(
2374+
'/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
2375+
$key,
2376+
$matches
2377+
)
2378+
) {
2379+
$startMergeInt = (int) $matches[2];
2380+
$endMergeInt = (int) $matches[4];
2381+
if ($startMergeInt >= $startRow) {
2382+
if ($startMergeInt <= $endRow) {
2383+
$removeKeys[] = $key;
2384+
}
2385+
} elseif ($endMergeInt >= $startRow) {
2386+
if ($endMergeInt <= $endRow) {
2387+
$temp = $endMergeInt - 1;
2388+
$removeKeys[] = $key;
2389+
if ($temp !== $startMergeInt) {
2390+
$temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp;
2391+
$addKeys[] = $temp3;
2392+
}
2393+
}
2394+
}
2395+
}
2396+
}
2397+
foreach ($removeKeys as $key) {
2398+
unset($this->mergeCells[$key]);
2399+
}
2400+
foreach ($addKeys as $key) {
2401+
$this->mergeCells[$key] = $key;
2402+
}
23662403

23672404
$holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
23682405
$highestRow = $this->getHighestDataRow();
@@ -2419,6 +2456,43 @@ public function removeColumn(string $column, int $numberOfColumns = 1): static
24192456
if (is_numeric($column)) {
24202457
throw new Exception('Column references should not be numeric.');
24212458
}
2459+
$startColumnInt = Coordinate::columnIndexFromString($column);
2460+
$endColumnInt = $startColumnInt + $numberOfColumns - 1;
2461+
$removeKeys = [];
2462+
$addKeys = [];
2463+
foreach ($this->mergeCells as $key => $value) {
2464+
if (
2465+
Preg::isMatch(
2466+
'/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
2467+
$key,
2468+
$matches
2469+
)
2470+
) {
2471+
$startMergeInt = Coordinate::columnIndexFromString($matches[1]);
2472+
$endMergeInt = Coordinate::columnIndexFromString($matches[3]);
2473+
if ($startMergeInt >= $startColumnInt) {
2474+
if ($startMergeInt <= $endColumnInt) {
2475+
$removeKeys[] = $key;
2476+
}
2477+
} elseif ($endMergeInt >= $startColumnInt) {
2478+
if ($endMergeInt <= $endColumnInt) {
2479+
$temp = Coordinate::columnIndexFromString($matches[3]) - 1;
2480+
$temp2 = Coordinate::stringFromColumnIndex($temp);
2481+
$removeKeys[] = $key;
2482+
if ($temp2 !== $matches[1]) {
2483+
$temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4];
2484+
$addKeys[] = $temp3;
2485+
}
2486+
}
2487+
}
2488+
}
2489+
}
2490+
foreach ($removeKeys as $key) {
2491+
unset($this->mergeCells[$key]);
2492+
}
2493+
foreach ($addKeys as $key) {
2494+
$this->mergeCells[$key] = $key;
2495+
}
24222496

24232497
$highestColumn = $this->getHighestDataColumn();
24242498
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
@@ -3584,7 +3658,7 @@ public function hasCodeName(): bool
35843658

35853659
public static function nameRequiresQuotes(string $sheetName): bool
35863660
{
3587-
return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1;
3661+
return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
35883662
}
35893663

35903664
public function isRowVisible(int $row): bool
@@ -3716,7 +3790,7 @@ public function calculateArrays(bool $preCalculateFormulas = true): void
37163790
$keys = $this->cellCollection->getCoordinates();
37173791
foreach ($keys as $key) {
37183792
if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) {
3719-
if (preg_match(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValue()) !== 1) {
3793+
if (!Preg::isMatch(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValue())) {
37203794
$this->getCell($key)->getCalculatedValue();
37213795
}
37223796
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
6+
7+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
use PhpOffice\PhpSpreadsheet\Style\Alignment;
10+
use PhpOffice\PhpSpreadsheet\Style\Fill;
11+
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
12+
use PHPUnit\Framework\TestCase;
13+
14+
class MergeCellsDeletedTest extends TestCase
15+
{
16+
public function testDeletedColumns(): void
17+
{
18+
$infile = 'tests/data/Reader/XLSX/issue.282.xlsx';
19+
$reader = new XlsxReader();
20+
$spreadsheet = $reader->load($infile);
21+
$sheet = $spreadsheet->getSheetByNameOrThrow('Sheet1');
22+
23+
$mergeCells = $sheet->getMergeCells();
24+
self::assertSame(['B1:F1', 'G1:I1'], array_values($mergeCells));
25+
26+
// Want to delete column B,C,D,E,F
27+
$sheet->removeColumnByIndex(2, 5);
28+
$mergeCells2 = $sheet->getMergeCells();
29+
self::assertSame(['B1:D1'], array_values($mergeCells2));
30+
$spreadsheet->disconnectWorksheets();
31+
}
32+
33+
public function testDeletedRows(): void
34+
{
35+
$infile = 'tests/data/Reader/XLSX/issue.282.xlsx';
36+
$reader = new XlsxReader();
37+
$spreadsheet = $reader->load($infile);
38+
$sheet = $spreadsheet->getSheetByNameOrThrow('Sheet2');
39+
40+
$mergeCells = $sheet->getMergeCells();
41+
self::assertSame(['A2:A6', 'A7:A9'], array_values($mergeCells));
42+
43+
// Want to delete rows 2 to 4
44+
$sheet->removeRow(2, 3);
45+
$mergeCells2 = $sheet->getMergeCells();
46+
self::assertSame(['A4:A6'], array_values($mergeCells2));
47+
$spreadsheet->disconnectWorksheets();
48+
}
49+
50+
private static function yellowBackground(Worksheet $sheet, string $cells, string $color = 'ffffff00'): void
51+
{
52+
$sheet->getStyle($cells)
53+
->getFill()
54+
->setFillType(Fill::FILL_SOLID);
55+
$sheet->getStyle($cells)
56+
->getFill()
57+
->getStartColor()
58+
->setArgb($color);
59+
$sheet->getStyle($cells)
60+
->getAlignment()
61+
->setHorizontal(Alignment::HORIZONTAL_CENTER);
62+
}
63+
64+
public static function testDeletedColumns2(): void
65+
{
66+
$spreadsheet = new Spreadsheet();
67+
$sheet = $spreadsheet->getActiveSheet();
68+
$sheet->setTitle('Before');
69+
$sheet->getCell('A1')->setValue('a1');
70+
$sheet->getCell('J1')->setValue('j1');
71+
$sheet->getCell('K1')->setValue('will delete d-f');
72+
$sheet->getCell('C1')->setValue('c1-g1');
73+
$sheet->mergeCells('C1:G1');
74+
self::yellowBackground($sheet, 'C1');
75+
76+
$sheet->getCell('A2')->setValue('a2');
77+
$sheet->getCell('J2')->setValue('j2');
78+
$sheet->getCell('B2')->setValue('b2-c2');
79+
$sheet->mergeCells('B2:C2');
80+
self::yellowBackground($sheet, 'B2');
81+
$sheet->getCell('G2')->setValue('g2-h2');
82+
$sheet->mergeCells('G2:H2');
83+
self::yellowBackground($sheet, 'G2', 'FF00FFFF');
84+
85+
$sheet->getCell('A3')->setValue('a3');
86+
$sheet->getCell('J3')->setValue('j3');
87+
$sheet->getCell('D3')->setValue('d3-g3');
88+
$sheet->mergeCells('D3:G3');
89+
self::yellowBackground($sheet, 'D3');
90+
91+
$sheet->getCell('A4')->setValue('a4');
92+
$sheet->getCell('J4')->setValue('j4');
93+
$sheet->getCell('B4')->setValue('b4-d4');
94+
$sheet->mergeCells('B4:D4');
95+
self::yellowBackground($sheet, 'B4');
96+
97+
$sheet->getCell('A5')->setValue('a5');
98+
$sheet->getCell('J5')->setValue('j5');
99+
$sheet->getCell('D5')->setValue('d5-e5');
100+
$sheet->mergeCells('D5:E5');
101+
self::yellowBackground($sheet, 'D5');
102+
103+
$sheet->removeColumn('D', 3);
104+
$expected = [
105+
'C1:D1', // was C1:G1, drop 3 inside cells
106+
'B2:C2', // was B2:C2, unaffected
107+
'D2:E2', // was G2:H2, move 3 columns left
108+
//'D2:E2', // was D3:G3, start in delete range
109+
'B4:C4', // was B4:D4, truncated at start of delete range
110+
//'D5:E5', // was D5:E5, start in delete range
111+
];
112+
self::assertSame($expected, array_keys($sheet->getMergeCells()));
113+
114+
$spreadsheet->disconnectWorksheets();
115+
}
116+
117+
public static function testDeletedRows2(): void
118+
{
119+
$spreadsheet = new Spreadsheet();
120+
$sheet = $spreadsheet->getActiveSheet();
121+
$sheet->setTitle('Before');
122+
$sheet->getCell('A1')->setValue('a1');
123+
$sheet->getCell('A10')->setValue('a10');
124+
$sheet->getCell('A11')->setValue('will delete 4-6');
125+
$sheet->getCell('A3')->setValue('a3-a7');
126+
$sheet->mergeCells('A3:A7');
127+
self::yellowBackground($sheet, 'A3');
128+
129+
$sheet->getCell('B1')->setValue('b1');
130+
$sheet->getCell('B10')->setValue('b10');
131+
$sheet->getCell('B2')->setValue('b2-b3');
132+
$sheet->mergeCells('B2:B3');
133+
self::yellowBackground($sheet, 'B2');
134+
$sheet->getCell('B7')->setValue('b7-b8');
135+
$sheet->mergeCells('B7:B8');
136+
self::yellowBackground($sheet, 'B7', 'FF00FFFF');
137+
138+
$sheet->getCell('C1')->setValue('c1');
139+
$sheet->getCell('C10')->setValue('c10');
140+
$sheet->getCell('C4')->setValue('c4-c7');
141+
$sheet->mergeCells('C4:C7');
142+
self::yellowBackground($sheet, 'C4');
143+
144+
$sheet->getCell('D1')->setValue('d1');
145+
$sheet->getCell('D10')->setValue('d10');
146+
$sheet->getCell('D2')->setValue('d2-d4');
147+
$sheet->mergeCells('D2:D4');
148+
self::yellowBackground($sheet, 'd2');
149+
150+
$sheet->getCell('E1')->setValue('e1');
151+
$sheet->getCell('E10')->setValue('e10');
152+
$sheet->getCell('E4')->setValue('e4-e5');
153+
$sheet->mergeCells('E4:E5');
154+
self::yellowBackground($sheet, 'E4');
155+
156+
$sheet->removeRow(4, 3);
157+
$expected = [
158+
'A3:A4', // was A3:A7, drop 3 inside cells
159+
'B2:B3', // was B2:B3, unaffected
160+
'B4:B5', // was B7:B8, move 3 columns up
161+
//'C4:C7', // was C4:C7, start in delete range
162+
'D2:D3', // was D2:D4, truncated at start of delete range
163+
//'E4:E5', // was E4:E5, start in delete range
164+
];
165+
self::assertSame($expected, array_keys($sheet->getMergeCells()));
166+
167+
$spreadsheet->disconnectWorksheets();
168+
}
169+
}

tests/data/Reader/XLSX/issue.282.xlsx

10.9 KB
Binary file not shown.

0 commit comments

Comments
 (0)