diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 2ba3f08187..34eed3a434 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3613,4 +3613,96 @@ public function copyCells(string $fromCell, string $toCells, bool $copyStyle = t } } } + + /** + * Copy a range area, with merged cells applied to another location. + * Acts similarly to Excel copy/paste merged areas to specific cell. + * + * @param string $sourceRange cell range, e.g. C3:C10 + * @param int $repetitions the number of times to repeat the source range + * @param int $groupSize the number of cells in the source range to repeat + */ + public function repeatSourceRange(string $sourceRange, int $repetitions = 2, int $groupSize = 2): void + { + // Get the start and end coordinates of the source range + [$sourceStart, $sourceEnd] = Coordinate::rangeBoundaries($sourceRange); + $sourceStartColumnIndex = $sourceStart[0]; + $sourceEndColumnIndex = $sourceEnd[0]; + + // Find the merged cells within the source range + $mergedCellRanges = $this->getMergeCellsFromCollection($sourceStart, $sourceEnd); + + // Copy the cells and merge them in the new locations + $groupCount = 0; + $rowOffset = 0; + + for ($i = 1; $i <= $repetitions + 1; ++$i) { + if ($i % $groupSize != 1 && $i > $groupSize) { + $offset = ($i - 1) % $groupSize * ($sourceEndColumnIndex - $sourceStartColumnIndex + 1); + } else { + $offset = ($i - 1) % $groupSize * ($sourceEndColumnIndex - $sourceStartColumnIndex + 2); + } + + if ($i % $groupSize != 1) { + $offset += $groupCount; + } + + // Copy the styles + foreach ($this->rangeToArray($sourceRange, false, true, true, true) as $row => $columns) { + foreach ($columns as $column => $cell) { + $coordinate = $column . $row; + $destinationCoordinate = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($column) + $offset) . ($row + $rowOffset); + $this->duplicateStyle($this->getStyle($coordinate), $destinationCoordinate); + } + } + + // Merge the cells + $this->mergeCellsOfTheRanges($mergedCellRanges, $offset, $rowOffset); + + // Increase group count and row offset after every groupSize repetitions + if ($i % $groupSize == 0) { + $rowOffset += $sourceEnd[1] - $sourceStart[1] + 2; // 2 is the number of rows to skip + ++$groupCount; + } + } + } + + /** + * Will get the coordinates to the Start/End Cell with Column and Row indexes. + * + * @param array $sourceStart Source Start column/row index + * @param array $sourceEnd Source End column/row index + */ + private function getMergeCellsFromCollection(array $sourceStart, array $sourceEnd): array + { + $mergeCells = []; + foreach ($this->getMergeCells() as $mergedCellRange) { + [$mergedStart, $mergedEnd] = Coordinate::rangeBoundaries($mergedCellRange); + if ($mergedStart[0] >= $sourceStart[0] && $mergedEnd[0] <= $sourceEnd[0] && $mergedStart[1] >= $sourceStart[1] && $mergedEnd[1] <= $sourceEnd[1]) { + $mergedCellRanges[] = $mergedCellRange; + } + } + + return $mergeCells; + } + + /** + * Will get the coordinates to the start/end cell with column and row indexes. + * + * @param array $mergedCellRanges array of merged cell ranges + * @param int $offset separation between the source and destination ranges by column + * @param int $rowOffset separation of group index with cell collection by row + */ + private function mergeCellsOfTheRanges(array $mergedCellRanges, int $offset, int $rowOffset): void + { + foreach ($mergedCellRanges as $mergedCellRange) { + [$mergedStart, $mergedEnd] = Coordinate::rangeBoundaries($mergedCellRange); + $mergedStartColumnIndex = $mergedStart[0]; + $mergedEndColumnIndex = $mergedEnd[0]; + $destinationStartColumn = Coordinate::stringFromColumnIndex($mergedStartColumnIndex + $offset); + $destinationEndColumn = Coordinate::stringFromColumnIndex($mergedEndColumnIndex + $offset); + $destinationRange = $destinationStartColumn . ($mergedStart[1] + $rowOffset) . ':' . $destinationEndColumn . ($mergedEnd[1] + $rowOffset); + $this->mergeCells($destinationRange); + } + } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index 87e7eab612..578fc83ae1 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -625,4 +625,39 @@ public static function rangeToArrayProvider(): array ], ]; } + + public function testRepeatSourceRangeBasic(): void + { + $spreadsheet = new Spreadsheet(); // Create a Spreadsheet + $worksheet = $spreadsheet->getActiveSheet(); // Get the active Worksheet + + // Add cells to the worksheet + $worksheet->getCell('A1')->setValue('Test1'); + $worksheet->getCell('B1')->setValue('Test2'); + $worksheet->getCell('A2')->setValue('Test3'); + $worksheet->getCell('B2')->setValue('Test4'); + $worksheet->getCell('A3')->setValue('Test5'); + $worksheet->getCell('B3')->setValue('Test6'); + + // Add merged cells to the worksheet + $worksheet->mergeCells('A1:B1'); + $worksheet->mergeCells('A2:B2'); + $worksheet->mergeCells('A3:B3'); + + $sourceRange = 'A1:B3'; + $repetitions = 2; + $groupSize = 1; + + $worksheet->repeatSourceRange($sourceRange, $repetitions, $groupSize); + + // Assert that cells are copied correctly + self::assertEquals('Test1', $worksheet->getCell('A1')->getValue()); + self::assertEquals('Test3', $worksheet->getCell('A2')->getValue()); + self::assertEquals('Test5', $worksheet->getCell('A3')->getValue()); + + // Assert that merged cells are copied correctly + self::assertContains('A1:B1', $worksheet->getMergeCells()); + self::assertContains('A2:B2', $worksheet->getMergeCells()); + self::assertContains('A3:B3', $worksheet->getMergeCells()); + } }