From 8ecf69a5c4469111ed5adb58421e442f5eadfdbe Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 15 Sep 2022 21:07:31 +0200 Subject: [PATCH] Handle additional merge options like those provide in OpenOffice or LibreOffice to hide cell values in a merge range rather than empty them, or to merge the values as well as the cells This includes reading hidden values in merge ranges, so that Unmerging can restore their visibility --- CHANGELOG.md | 6 +- .../images/12-01-MergeCells-Options-2.png | Bin 0 -> 3313 bytes .../images/12-01-MergeCells-Options-3.png | Bin 0 -> 5029 bytes .../images/12-01-MergeCells-Options.png | Bin 0 -> 22477 bytes docs/topics/recipes.md | 60 ++++++- src/PhpSpreadsheet/Reader/Gnumeric.php | 2 +- src/PhpSpreadsheet/Reader/Ods.php | 3 +- src/PhpSpreadsheet/Reader/Xls.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 2 +- src/PhpSpreadsheet/Reader/Xml.php | 3 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 76 ++++++-- .../Worksheet/MergeBehaviourTest.php | 166 ++++++++++++++++++ 12 files changed, 291 insertions(+), 29 deletions(-) create mode 100644 docs/topics/images/12-01-MergeCells-Options-2.png create mode 100644 docs/topics/images/12-01-MergeCells-Options-3.png create mode 100644 docs/topics/images/12-01-MergeCells-Options.png create mode 100644 tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dc17d852..f15e9d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Changed -- Nothing +- Allow variant behaviour when merging cells [Issue #3065](https://github.com/PHPOffice/PhpSpreadsheet/issues/3065) + - Merge methods now allow an additional `$behaviour` argument. Permitted values are: + - Worksheet::MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells (the default behaviour) + - Worksheet::MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + - Worksheet::MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell ### Deprecated diff --git a/docs/topics/images/12-01-MergeCells-Options-2.png b/docs/topics/images/12-01-MergeCells-Options-2.png new file mode 100644 index 0000000000000000000000000000000000000000..5e745fc934dc88c114b39dc1a45a48ddd88ad0aa GIT binary patch literal 3313 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 literal 0 HcmV?d00001 diff --git a/docs/topics/images/12-01-MergeCells-Options-3.png b/docs/topics/images/12-01-MergeCells-Options-3.png new file mode 100644 index 0000000000000000000000000000000000000000..30ad346eac43344b9200a9c49928209a71663e35 GIT binary patch literal 5029 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 Qf|tw0@Tft#zFXY?0K=D0BLDyZ literal 0 HcmV?d00001 diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index afa3dcc8..04a4e6aa 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1332,22 +1332,72 @@ rows (default), or above. The following code adds the summary above: $spreadsheet->getActiveSheet()->setShowSummaryBelow(false); ``` -## Merge/unmerge cells +## Merge/Unmerge cells -If you have a big piece of data you want to display in a worksheet, you -can merge two or more cells together, to become one cell. This can be -done using the following code: +If you have a big piece of data you want to display in a worksheet, or a +heading that needs to span multiple sub-heading columns, you can merge +two or more cells together, to become one cell. This can be done using +the following code: ```php $spreadsheet->getActiveSheet()->mergeCells('A18:E22'); ``` -Removing a merge can be done using the unmergeCells method: +Removing a merge can be done using the `unmergeCells()` method: ```php $spreadsheet->getActiveSheet()->unmergeCells('A18:E22'); ``` +MS Excel itself doesn't yet offer the functionality to simply hide the merged cells, or to merge the content of cells into a single cell, but it is available in Open/Libre Office. + +### Merge with MERGE_CELL_CONTENT_EMPTY + +The default behaviour is to empty all cells except for the top-left corner cell in the merge range; and this is also the default behaviour for the `mergeCells()` method in PhpSpreadsheet. +When this behaviour is applied, those cell values will be set to null; and if they are subsequently Unmerged, they will be empty cells. + +Passing an extra flag value to the `mergeCells()` method in PhpSpreadsheet can change this behaviour. + +![12-01-MergeCells-Options.png](./images/12-01-MergeCells-Options.png) + +Possible flag values are: +- Worksheet::MERGE_CELL_CONTENT_EMPTY (the default) +- Worksheet::MERGE_CELL_CONTENT_HIDE +- Worksheet::MERGE_CELL_CONTENT_MERGE + +### Merge with MERGE_CELL_CONTENT_HIDE + +The first alternative, available only in OpenOffice, is to hide those cells, but to leave their content intact. +When a file saved as `Xlsx` in those applications is opened in MS Excel, and those cells are unmerged, the original content will still be present. + +```php +$spreadsheet->getActiveSheet()->mergeCells('A1:C3', Worksheet::MERGE_CELL_CONTENT_HIDE); +``` + +Will replicate that behaviour. + +### Merge with MERGE_CELL_CONTENT_MERGE + +The second alternative, available in both OpenOffice and LibreOffice is to merge the content of every cell in the merge range into the top-left cell, while setting those hidden cells to empty. + +```php +$spreadsheet->getActiveSheet()->mergeCells('A1:C3', Worksheet::MERGE_CELL_CONTENT_MERGE); +``` + +Particularly when the merged cells contain formulae, the logic for this merge seems strange: +walking through the merge range, each cell is calculated in turn, and appended to the "master" cell, then it is emptied, so any subsequent calculations that reference the cell see an empty cell, not the pre-merge value. +For example, suppose our spreadsheet contains + +![12-01-MergeCells-Options-2.png](./images/12-01-MergeCells-Options-2.png) + +where `B2` is the formula `=5-B1` and `C2` is the formula `=A2/B2`, +and we want to merge cells `A2` to `C2` with all the cell values merged. +The result is: + +![12-01-MergeCells-Options-3.png](./images/12-01-MergeCells-Options-3.png) + +The cell value `12` from cell `A2` is fixed; the value from `B2` is the result of the formula `=5-B1` (`4`, which is appended to our merged value), and cell `B2` is then emptied, so when we evaluate cell `C2` with the formula `=A2/B2` it gives us `12 / 0` which results in a `#DIV/0!` error (so the error `#DIV/0!` is appended to our merged value rather than the original calculation result of `3`). + ## Inserting or Removing rows/columns You can insert/remove rows/columns at a specific position. The following diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index ca087e61..1dcb0a12 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -363,7 +363,7 @@ class Gnumeric extends BaseReader if ($sheet !== null && isset($sheet->MergedRegions)) { foreach ($sheet->MergedRegions->Merge as $mergeCells) { if (strpos((string) $mergeCells, ':') !== false) { - $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); + $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 7e776ab7..e3de4731 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -20,6 +20,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; use XMLReader; use ZipArchive; @@ -759,7 +760,7 @@ class Ods extends BaseReader } $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 71496ece..a8de5228 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4585,7 +4585,7 @@ class Xls extends BaseReader (strpos($cellRangeAddress, ':') !== false) && ($this->includeCellRangeFiltered($cellRangeAddress)) ) { - $this->phpSheet->mergeCells($cellRangeAddress); + $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index fc38375a..26cd1af3 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -914,7 +914,7 @@ class Xlsx extends BaseReader foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) { $mergeRef = (string) $mergeCell['ref']; if (strpos($mergeRef, ':') !== false) { - $docSheet->mergeCells((string) $mergeCell['ref']); + $docSheet->mergeCells((string) $mergeCell['ref'], Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 0b5e0966..d8f0d9dc 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -18,6 +18,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; /** @@ -364,7 +365,7 @@ class Xml extends BaseReader $rowTo = $rowTo + $cell_ss['MergeDown']; } $cellRange .= ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } $hasCalculatedValue = false; diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d13d4141..55327932 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -41,6 +41,10 @@ class Worksheet implements IComparable public const SHEETSTATE_HIDDEN = 'hidden'; public const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + public const MERGE_CELL_CONTENT_EMPTY = 'empty'; + public const MERGE_CELL_CONTENT_HIDE = 'hide'; + public const MERGE_CELL_CONTENT_MERGE = 'merge'; + protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui'; /** @@ -1758,10 +1762,15 @@ class Worksheet implements IComparable * @param AddressRange|array|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCells($range) + public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); @@ -1793,18 +1802,22 @@ class Worksheet implements IComparable $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); } - // Blank out the rest of the cells in the range (if they exist) - if ($numberRows > $numberColumns) { - $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft); - } else { - $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft); + if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) { + // Blank out the rest of the cells in the range (if they exist) + if ($numberRows > $numberColumns) { + $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour); + } else { + $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour); + } } return $this; } - private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft): void + private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) { $iterator = $column->getCellIterator($firstRow); $iterator->setIterateOnlyExistingCells(true); @@ -1814,17 +1827,21 @@ class Worksheet implements IComparable if ($row > $lastRow) { break; } - $thisCell = $cell->getColumn() . $row; - if ($upperLeft !== $thisCell) { - $cell->setValueExplicit(null, DataType::TYPE_NULL); - } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); } } } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } } - private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft): void + private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + foreach ($this->getRowIterator($firstRow, $lastRow) as $row) { $iterator = $row->getCellIterator($firstColumn); $iterator->setIterateOnlyExistingCells(true); @@ -1835,13 +1852,31 @@ class Worksheet implements IComparable if ($columnIndex > $lastColumnIndex) { break; } - $thisCell = $column . $cell->getRow(); - if ($upperLeft !== $thisCell) { - $cell->setValueExplicit(null, DataType::TYPE_NULL); - } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); } } } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } + } + + public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array + { + if ($cell->getCoordinate() !== $upperLeft) { + Calculation::getInstance($cell->getWorksheet()->getParent())->flushInstance(); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $cellValue = $cell->getFormattedValue(); + if ($cellValue !== '') { + $leftCellValue[] = $cellValue; + } + } + $cell->setValueExplicit(null, DataType::TYPE_NULL); + } + + return $leftCellValue; } /** @@ -1856,17 +1891,22 @@ class Worksheet implements IComparable * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $row2 Numeric row coordinate of the last cell + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { $cellRange = new CellRange( CellAddress::fromColumnAndRow($columnIndex1, $row1), CellAddress::fromColumnAndRow($columnIndex2, $row2) ); - return $this->mergeCells($cellRange); + return $this->mergeCells($cellRange, $behaviour); } /** diff --git a/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php b/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php new file mode 100644 index 00000000..68c9d87a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php @@ -0,0 +1,166 @@ +getActiveSheet(); + $worksheet->fromArray($this->testDataRaw, null, 'A1', true); + $worksheet->mergeCells($mergeRange); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsDefaultBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19', null], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsHideBehaviour(): void + { + $expectedResult = [ + [1.1, 2.2, 3.3], + [4.4, 5.5, 9.9], + [5.5, 7.7, 13.2], + ]; + + $mergeRange = 'A1:C3'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataRaw, null, 'A1', true); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsHideBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19', '2022-09-15'], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + /** + * @dataProvider mergeCellsMergeBehaviourProvider + */ + public function testMergeCellsMergeBehaviour(array $testData, string $mergeRange, array $expectedResult): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($testData, null, 'A1', true); + // Force a precalculation to populate the calculation cache, so that we can verify that it is being cleared + $worksheet->toArray(); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_MERGE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function mergeCellsMergeBehaviourProvider(): array + { + return [ + 'With Calculated Values' => [ + $this->testDataRaw, + 'A1:C3', + [ + ['1.1 2.2 1.1 4.4 5.5 0 1.1 0 0', null, null], + [null, null, null], + [null, null, null], + ], + ], + 'With Empty Cells' => [ + [ + [1, '', 2], + [null, 3, null], + [4, null, 5], + ], + 'A1:C3', + [ + ['1 2 3 4 5', null, null], + [null, null, null], + [null, null, null], + ], + ], + [ + [ + [12, '=5+1', '=A1/A2'], + ], + 'A1:C1', + [ + ['12 6 #DIV/0!', null, null], + ], + ], + ]; + } + + public function testMergeCellsMergeBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19 2022-09-15', null], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_MERGE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } +}