Skip to content

Commit 6786cd5

Browse files
committed
Something More Sophisticated
When the merged cells start outside the deleted range but end inside it, the merge is truncated to that portion which is outside the range.
1 parent bc54cfd commit 6786cd5

File tree

2 files changed

+185
-15
lines changed

2 files changed

+185
-15
lines changed

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 61 additions & 15 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;
@@ -1205,8 +1206,8 @@ private function getWorksheetAndCoordinate(string $coordinate): array
12051206
throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
12061207
}
12071208
} elseif (
1208-
!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
1209-
&& preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
1209+
!Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
1210+
&& Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
12101211
) {
12111212
// Named range?
12121213
$namedRange = $this->validateNamedRange($coordinate, true);
@@ -1458,7 +1459,7 @@ public function getTablesWithStylesForCell(Cell $cell): array
14581459
public function getConditionalStyles(string $coordinate, bool $firstOnly = true): array
14591460
{
14601461
$coordinate = strtoupper($coordinate);
1461-
if (preg_match('/[: ,]/', $coordinate) === 1) {
1462+
if (Preg::isMatch('/[: ,]/', $coordinate)) {
14621463
return $this->conditionalStylesCollection[$coordinate] ?? [];
14631464
}
14641465

@@ -1778,7 +1779,7 @@ public function mergeCells(AddressRange|string|array $range, string $behaviour =
17781779
$range .= ":{$range}";
17791780
}
17801781

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

@@ -2395,17 +2396,39 @@ public function removeRow(int $row, int $numberOfRows = 1): static
23952396
$startRow = $row;
23962397
$endRow = $startRow + $numberOfRows - 1;
23972398
$removeKeys = [];
2399+
$addKeys = [];
23982400
foreach ($this->mergeCells as $key => $value) {
2399-
if (preg_match('/^[a-z]{1,3}(\d+)/i', $key, $matches) === 1) {
2400-
$startMergeInt = (int) $matches[1];
2401-
if ($startMergeInt >= $startRow && $startMergeInt <= $endRow) {
2402-
$removeKeys[] = $key;
2401+
if (
2402+
Preg::isMatch(
2403+
'/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
2404+
$key,
2405+
$matches
2406+
)
2407+
) {
2408+
$startMergeInt = (int) $matches[2];
2409+
$endMergeInt = (int) $matches[4];
2410+
if ($startMergeInt >= $startRow) {
2411+
if ($startMergeInt <= $endRow) {
2412+
$removeKeys[] = $key;
2413+
}
2414+
} elseif ($endMergeInt >= $startRow) {
2415+
if ($endMergeInt <= $endRow) {
2416+
$temp = $endMergeInt - 1;
2417+
$removeKeys[] = $key;
2418+
if ($temp !== $startMergeInt) {
2419+
$temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp;
2420+
$addKeys[] = $temp3;
2421+
}
2422+
}
24032423
}
24042424
}
24052425
}
24062426
foreach ($removeKeys as $key) {
24072427
unset($this->mergeCells[$key]);
24082428
}
2429+
foreach ($addKeys as $key) {
2430+
$this->mergeCells[$key] = $key;
2431+
}
24092432

24102433
$holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
24112434
$highestRow = $this->getHighestDataRow();
@@ -2465,17 +2488,40 @@ public function removeColumn(string $column, int $numberOfColumns = 1): static
24652488
$startColumnInt = Coordinate::columnIndexFromString($column);
24662489
$endColumnInt = $startColumnInt + $numberOfColumns - 1;
24672490
$removeKeys = [];
2491+
$addKeys = [];
24682492
foreach ($this->mergeCells as $key => $value) {
2469-
if (preg_match('/^[a-z]{1,3}/i', $key, $matches) === 1) {
2470-
$startMergeInt = Coordinate::columnIndexFromString($matches[0]);
2471-
if ($startMergeInt >= $startColumnInt && $startMergeInt <= $endColumnInt) {
2472-
$removeKeys[] = $key;
2493+
if (
2494+
Preg::isMatch(
2495+
'/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
2496+
$key,
2497+
$matches
2498+
)
2499+
) {
2500+
$startMergeInt = Coordinate::columnIndexFromString($matches[1]);
2501+
$endMergeInt = Coordinate::columnIndexFromString($matches[3]);
2502+
if ($startMergeInt >= $startColumnInt) {
2503+
if ($startMergeInt <= $endColumnInt) {
2504+
$removeKeys[] = $key;
2505+
}
2506+
} elseif ($endMergeInt >= $startColumnInt) {
2507+
if ($endMergeInt <= $endColumnInt) {
2508+
$temp = Coordinate::columnIndexFromString($matches[3]) - 1;
2509+
$temp2 = Coordinate::stringFromColumnIndex($temp);
2510+
$removeKeys[] = $key;
2511+
if ($temp2 !== $matches[1]) {
2512+
$temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4];
2513+
$addKeys[] = $temp3;
2514+
}
2515+
}
24732516
}
24742517
}
24752518
}
24762519
foreach ($removeKeys as $key) {
24772520
unset($this->mergeCells[$key]);
24782521
}
2522+
foreach ($addKeys as $key) {
2523+
$this->mergeCells[$key] = $key;
2524+
}
24792525

24802526
$highestColumn = $this->getHighestDataColumn();
24812527
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
@@ -3464,7 +3510,7 @@ public function getDataValidationCollection(): array
34643510
$collectionCells = [];
34653511
$collectionRanges = [];
34663512
foreach ($this->dataValidationCollection as $key => $dataValidation) {
3467-
if (preg_match('/[: ]/', $key) === 1) {
3513+
if (Preg::isMatch('/[: ]/', $key)) {
34683514
$collectionRanges[$key] = $dataValidation;
34693515
} else {
34703516
$collectionCells[$key] = $dataValidation;
@@ -3732,7 +3778,7 @@ public function hasCodeName(): bool
37323778

37333779
public static function nameRequiresQuotes(string $sheetName): bool
37343780
{
3735-
return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1;
3781+
return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
37363782
}
37373783

37383784
public function isRowVisible(int $row): bool
@@ -3865,7 +3911,7 @@ public function calculateArrays(bool $preCalculateFormulas = true): void
38653911
$keys = $this->cellCollection->getCoordinates();
38663912
foreach ($keys as $key) {
38673913
if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) {
3868-
if (preg_match(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValueString()) !== 1) {
3914+
if (!Preg::isMatch(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValueString())) {
38693915
$this->getCell($key)->getCalculatedValue();
38703916
}
38713917
}

tests/PhpSpreadsheetTests/Worksheet/MergeCellsDeletedTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
66

77
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;
812
use PHPUnit\Framework\TestCase;
913

1014
class MergeCellsDeletedTest extends TestCase
@@ -42,4 +46,124 @@ public function testDeletedRows(): void
4246
self::assertSame(['A4:A6'], array_values($mergeCells2));
4347
$spreadsheet->disconnectWorksheets();
4448
}
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+
}
45169
}

0 commit comments

Comments
 (0)