diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 896b4e9c..51cf3c52 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1698,27 +1698,33 @@ class Worksheet implements IComparable { // Uppercase coordinate $range = strtoupper($range); + // Convert 'A:C' to 'A1:C1048576' + $range = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $range); + // Convert '1:3' to 'A1:XFD3' + $range = self::pregReplace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $range); - if (strpos($range, ':') !== false) { + if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) === 1) { $this->mergeCells[$range] = $range; - - // make sure cells are created - - // get the cells in the range - $aReferences = Coordinate::extractAllCellReferencesInRange($range); + $firstRow = (int) $matches[2]; + $lastRow = (int) $matches[4]; + $firstColumn = $matches[1]; + $lastColumn = $matches[3]; + $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn); + $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn); + $numberRows = $lastRow - $firstRow; + $numberColumns = $lastColumnIndex - $firstColumnIndex; // create upper left cell if it does not already exist - $upperLeft = $aReferences[0]; + $upperLeft = "$firstColumn$firstRow"; if (!$this->cellExists($upperLeft)) { $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); } // Blank out the rest of the cells in the range (if they exist) - $count = count($aReferences); - for ($i = 1; $i < $count; ++$i) { - if ($this->cellExists($aReferences[$i])) { - $this->getCell($aReferences[$i])->setValueExplicit(null, DataType::TYPE_NULL); - } + if ($numberRows > $numberColumns) { + $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft); + } else { + $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft); } } else { throw new Exception('Merge must be set on a range of cells.'); @@ -1727,6 +1733,47 @@ class Worksheet implements IComparable return $this; } + private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft): void + { + foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) { + $iterator = $column->getCellIterator($firstRow); + $iterator->setIterateOnlyExistingCells(true); + foreach ($iterator as $cell) { + if ($cell !== null) { + $row = $cell->getRow(); + if ($row > $lastRow) { + break; + } + $thisCell = $cell->getColumn() . $row; + if ($upperLeft !== $thisCell) { + $cell->setValueExplicit(null, DataType::TYPE_NULL); + } + } + } + } + } + + private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft): void + { + foreach ($this->getRowIterator($firstRow, $lastRow) as $row) { + $iterator = $row->getCellIterator($firstColumn); + $iterator->setIterateOnlyExistingCells(true); + foreach ($iterator as $cell) { + if ($cell !== null) { + $column = $cell->getColumn(); + $columnIndex = Coordinate::columnIndexFromString($column); + if ($columnIndex > $lastColumnIndex) { + break; + } + $thisCell = $column . $cell->getRow(); + if ($upperLeft !== $thisCell) { + $cell->setValueExplicit(null, DataType::TYPE_NULL); + } + } + } + } + } + /** * Set merge on a cell range by using numeric cell coordinates. * diff --git a/tests/PhpSpreadsheetTests/Calculation/MergedCellTest.php b/tests/PhpSpreadsheetTests/Calculation/MergedCellTest.php index 6a710436..f6e98474 100644 --- a/tests/PhpSpreadsheetTests/Calculation/MergedCellTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/MergedCellTest.php @@ -3,43 +3,43 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Exception as SpreadException; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class MergedCellTest extends TestCase { - /** - * @var Spreadsheet - */ - protected $spreadSheet; - - protected function setUp(): void - { - $this->spreadSheet = new Spreadsheet(); - - $dataSheet = $this->spreadSheet->getActiveSheet(); - $dataSheet->setCellValue('A1', 1.1); - $dataSheet->setCellValue('A2', 2.2); - $dataSheet->mergeCells('A2:A4'); - $dataSheet->setCellValue('A5', 3.3); - } - /** * @param mixed $expectedResult * - * @dataProvider providerWorksheetFormulae + * @dataProvider providerWorksheetFormulaeColumns */ - public function testMergedCellBehaviour(string $formula, $expectedResult): void + public function testMergedCellColumns(string $formula, $expectedResult): void { - $worksheet = $this->spreadSheet->getActiveSheet(); + $spreadSheet = new Spreadsheet(); + + $dataSheet = $spreadSheet->getActiveSheet(); + $dataSheet->setCellValue('A5', 3.3); + $dataSheet->setCellValue('A3', 3.3); + $dataSheet->setCellValue('A2', 2.2); + $dataSheet->setCellValue('A1', 1.1); + $dataSheet->setCellValue('B2', 2.2); + $dataSheet->setCellValue('B1', 1.1); + $dataSheet->setCellValue('C2', 4.4); + $dataSheet->setCellValue('C1', 3.3); + $dataSheet->mergeCells('A2:A4'); + $dataSheet->mergeCells('B:B'); + $worksheet = $spreadSheet->getActiveSheet(); $worksheet->setCellValue('A7', $formula); $result = $worksheet->getCell('A7')->getCalculatedValue(); self::assertSame($expectedResult, $result); + $spreadSheet->disconnectWorksheets(); } - public function providerWorksheetFormulae(): array + public function providerWorksheetFormulaeColumns(): array { return [ ['=SUM(A1:A5)', 6.6], @@ -48,6 +48,74 @@ class MergedCellTest extends TestCase ['=SUM(A3:A4)', 0], ['=A2+A3+A4', 2.2], ['=A2/A3', Functions::DIV0()], + ['=SUM(B1:C2)', 8.8], ]; } + + /** + * @param mixed $expectedResult + * + * @dataProvider providerWorksheetFormulaeRows + */ + public function testMergedCellRows(string $formula, $expectedResult): void + { + $spreadSheet = new Spreadsheet(); + + $dataSheet = $spreadSheet->getActiveSheet(); + $dataSheet->setCellValue('A1', 1.1); + $dataSheet->setCellValue('B1', 2.2); + $dataSheet->setCellValue('C1', 3.3); + $dataSheet->setCellValue('E1', 3.3); + $dataSheet->setCellValue('A2', 1.1); + $dataSheet->setCellValue('B2', 2.2); + $dataSheet->setCellValue('A3', 3.3); + $dataSheet->setCellValue('B3', 4.4); + $dataSheet->mergeCells('B1:D1'); + $dataSheet->mergeCells('A2:B2'); + $worksheet = $spreadSheet->getActiveSheet(); + + $worksheet->setCellValue('A7', $formula); + + $result = $worksheet->getCell('A7')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + $spreadSheet->disconnectWorksheets(); + } + + public function providerWorksheetFormulaeRows(): array + { + return [ + ['=SUM(A1:E1)', 6.6], + ['=COUNT(A1:E1)', 3], + ['=COUNTA(A1:E1)', 3], + ['=SUM(C1:D1)', 0], + ['=B1+C1+D1', 2.2], + ['=B1/C1', Functions::DIV0()], + ['=SUM(A2:B3)', 8.8], + ]; + } + + private function setBadRange(Worksheet $sheet, string $range): void + { + try { + $sheet->mergeCells($range); + self::fail("Expected invalid merge range $range"); + } catch (SpreadException $e) { + self::assertSame('Merge must be set on a range of cells.', $e->getMessage()); + } + } + + public function testMergedBadRange(): void + { + $spreadSheet = new Spreadsheet(); + + $dataSheet = $spreadSheet->getActiveSheet(); + $this->setBadRange($dataSheet, 'B1'); + $this->setBadRange($dataSheet, 'Invalid'); + $this->setBadRange($dataSheet, '1'); + $this->setBadRange($dataSheet, 'C'); + $this->setBadRange($dataSheet, 'B1:C'); + $this->setBadRange($dataSheet, 'B:C2'); + + $spreadSheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php new file mode 100644 index 00000000..6b090fe8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php @@ -0,0 +1,70 @@ +', $data); + } + $file = 'zip://'; + $file .= self::$testbook; + $file .= '#xl/worksheets/sheet2.xml'; + $data = file_get_contents($file); + // confirm that file contains expected merged ranges + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertStringContainsString('', $data); + } + } + + public function testIssue2501(): void + { + // Merged cell range specified as 1:1" + $filename = self::$testbook; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getSheetByName('Columns'); + $expected = [ + 'A1:A1048576', + 'B1:D1048576', + 'E2:E4', + ]; + if ($sheet === null) { + self::fail('Unable to find sheet Columns'); + } else { + self::assertSame($expected, array_values($sheet->getMergeCells())); + } + $sheet = $spreadsheet->getSheetByName('Rows'); + $expected = [ + 'A1:XFD1', + 'A2:XFD4', + 'B5:D5', + ]; + if ($sheet === null) { + self::fail('Unable to find sheet Rows'); + } else { + self::assertSame($expected, array_values($sheet->getMergeCells())); + } + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.2501.b.xlsx b/tests/data/Reader/XLSX/issue.2501.b.xlsx new file mode 100644 index 00000000..fddf197b Binary files /dev/null and b/tests/data/Reader/XLSX/issue.2501.b.xlsx differ