Skip to content

Commit 2219ded

Browse files
authored
Removing Columns/Rows Containing Merged Cells (#4472)
Backport PR #4465.
1 parent 126309f commit 2219ded

File tree

4 files changed

+250
-6
lines changed

4 files changed

+250
-6
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 #4353](https://github.com/PHPOffice/PhpSpreadsheet/pull/4353)
21+
- Removing Columns/Rows Containing Merged Cells. Backport of [PR #4465](https://github.com/PHPOffice/PhpSpreadsheet/pull/4465)
2122

2223
# 2025-02-07 - 2.1.9
2324

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 80 additions & 6 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;
@@ -1196,8 +1197,8 @@ private function getWorksheetAndCoordinate(string $coordinate): array
11961197
throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
11971198
}
11981199
} elseif (
1199-
!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
1200-
&& preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
1200+
!Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
1201+
&& Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
12011202
) {
12021203
// Named range?
12031204
$namedRange = $this->validateNamedRange($coordinate, true);
@@ -1699,7 +1700,7 @@ public function mergeCells(AddressRange|string|array $range, string $behaviour =
16991700
$range .= ":{$range}";
17001701
}
17011702

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

@@ -1726,9 +1727,9 @@ public function mergeCells(AddressRange|string|array $range, string $behaviour =
17261727
if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
17271728
// Blank out the rest of the cells in the range (if they exist)
17281729
if ($numberRows > $numberColumns) {
1729-
$this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
1730+
$this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour); //* @phpstan-ignore-line
17301731
} else {
1731-
$this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
1732+
$this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour); //* @phpstan-ignore-line
17321733
}
17331734
}
17341735

@@ -2335,6 +2336,42 @@ public function removeRow(int $row, int $numberOfRows = 1): static
23352336
if ($row < 1) {
23362337
throw new Exception('Rows to be deleted should at least start from row 1.');
23372338
}
2339+
$startRow = $row;
2340+
$endRow = $startRow + $numberOfRows - 1;
2341+
$removeKeys = [];
2342+
$addKeys = [];
2343+
foreach ($this->mergeCells as $key => $value) {
2344+
if (
2345+
Preg::isMatch(
2346+
'/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
2347+
$key,
2348+
$matches
2349+
)
2350+
) {
2351+
$startMergeInt = (int) $matches[2];
2352+
$endMergeInt = (int) $matches[4];
2353+
if ($startMergeInt >= $startRow) {
2354+
if ($startMergeInt <= $endRow) {
2355+
$removeKeys[] = $key;
2356+
}
2357+
} elseif ($endMergeInt >= $startRow) {
2358+
if ($endMergeInt <= $endRow) {
2359+
$temp = $endMergeInt - 1;
2360+
$removeKeys[] = $key;
2361+
if ($temp !== $startMergeInt) {
2362+
$temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp;
2363+
$addKeys[] = $temp3;
2364+
}
2365+
}
2366+
}
2367+
}
2368+
}
2369+
foreach ($removeKeys as $key) {
2370+
unset($this->mergeCells[$key]);
2371+
}
2372+
foreach ($addKeys as $key) {
2373+
$this->mergeCells[$key] = $key;
2374+
}
23382375

23392376
$holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
23402377
$highestRow = $this->getHighestDataRow();
@@ -2391,6 +2428,43 @@ public function removeColumn(string $column, int $numberOfColumns = 1): static
23912428
if (is_numeric($column)) {
23922429
throw new Exception('Column references should not be numeric.');
23932430
}
2431+
$startColumnInt = Coordinate::columnIndexFromString($column);
2432+
$endColumnInt = $startColumnInt + $numberOfColumns - 1;
2433+
$removeKeys = [];
2434+
$addKeys = [];
2435+
foreach ($this->mergeCells as $key => $value) {
2436+
if (
2437+
Preg::isMatch(
2438+
'/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
2439+
$key,
2440+
$matches
2441+
)
2442+
) {
2443+
$startMergeInt = Coordinate::columnIndexFromString($matches[1]);
2444+
$endMergeInt = Coordinate::columnIndexFromString($matches[3]);
2445+
if ($startMergeInt >= $startColumnInt) {
2446+
if ($startMergeInt <= $endColumnInt) {
2447+
$removeKeys[] = $key;
2448+
}
2449+
} elseif ($endMergeInt >= $startColumnInt) {
2450+
if ($endMergeInt <= $endColumnInt) {
2451+
$temp = Coordinate::columnIndexFromString($matches[3]) - 1;
2452+
$temp2 = Coordinate::stringFromColumnIndex($temp);
2453+
$removeKeys[] = $key;
2454+
if ($temp2 !== $matches[1]) {
2455+
$temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4];
2456+
$addKeys[] = $temp3;
2457+
}
2458+
}
2459+
}
2460+
}
2461+
}
2462+
foreach ($removeKeys as $key) {
2463+
unset($this->mergeCells[$key]);
2464+
}
2465+
foreach ($addKeys as $key) {
2466+
$this->mergeCells[$key] = $key;
2467+
}
23942468

23952469
$highestColumn = $this->getHighestDataColumn();
23962470
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
@@ -3535,7 +3609,7 @@ public function hasCodeName(): bool
35353609

35363610
public static function nameRequiresQuotes(string $sheetName): bool
35373611
{
3538-
return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1;
3612+
return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
35393613
}
35403614

35413615
public function isRowVisible(int $row): bool
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)