From 006e49765c641380dbf3bca12196358e1702bdd4 Mon Sep 17 00:00:00 2001 From: vo0ff <51830186+vo0ff@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:38:04 +0300 Subject: [PATCH] Bugzilla 69303: Fix RangeCopier's copyMergedRanges when copying withing the same sheet (#958) * Bugzilla 69303: added failing test for case with same sheet RangeCopier for merged regions * Bugzilla 69303: fixed same sheet issue when enabling copy of merged ranges * Bugzilla 69303: added @since 6.0.0 to new public methods --------- Co-authored-by: Viktor Ozerov --- .../apache/poi/ss/usermodel/RangeCopier.java | 15 ++++++++---- .../apache/poi/ss/util/CellRangeAddress.java | 14 +++++++++++ .../poi/ss/util/CellRangeAddressBase.java | 15 ++++++++++++ .../poi/ss/usermodel/BaseTestRangeCopier.java | 24 ++++++++++++++++--- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/poi/src/main/java/org/apache/poi/ss/usermodel/RangeCopier.java b/poi/src/main/java/org/apache/poi/ss/usermodel/RangeCopier.java index c6b82a6860..97aaa94e79 100644 --- a/poi/src/main/java/org/apache/poi/ss/usermodel/RangeCopier.java +++ b/poi/src/main/java/org/apache/poi/ss/usermodel/RangeCopier.java @@ -54,9 +54,11 @@ public abstract class RangeCopier { } /** Uses input pattern to tile destination region, overwriting existing content. Works in following manner : - * 1.Start from top-left of destination. - * 2.Paste source but only inside of destination borders. - * 3.If there is space left on right or bottom side of copy, process it as in step 2. + *
    + *
  1. Start from top-left of destination.
  2. + *
  3. Paste source but only inside of destination borders.
  4. + *
  5. If there is space left on right or bottom side of copy, process it as in step 2.
  6. + *
* @param tilePatternRange source range which should be copied in tiled manner * @param tileDestRange destination range, which should be overridden * @param copyStyles whether to copy the cell styles @@ -90,7 +92,12 @@ public abstract class RangeCopier { } while (nextRowIndexToCopy <= tileDestRange.getLastRow()); if (copyMergedRanges) { - sourceSheet.getMergedRegions().forEach((mergedRangeAddress) -> destSheet.addMergedRegion(mergedRangeAddress)); + int rowOffset = tileDestRange.getFirstRow() - tilePatternRange.getFirstRow(); + int columnOffset = tileDestRange.getFirstColumn() - tilePatternRange.getFirstColumn(); + sourceSheet.getMergedRegions().stream() + .filter(tilePatternRange::contains) + .map(sourceMergedRegion -> sourceMergedRegion.shift(rowOffset, columnOffset)) + .forEach(destSheet::addMergedRegion); } int tempCopyIndex = sourceSheet.getWorkbook().getSheetIndex(sourceCopy); diff --git a/poi/src/main/java/org/apache/poi/ss/util/CellRangeAddress.java b/poi/src/main/java/org/apache/poi/ss/util/CellRangeAddress.java index 91a60273a4..9cd1612d8f 100644 --- a/poi/src/main/java/org/apache/poi/ss/util/CellRangeAddress.java +++ b/poi/src/main/java/org/apache/poi/ss/util/CellRangeAddress.java @@ -133,4 +133,18 @@ public class CellRangeAddress extends CellRangeAddressBase { } return new CellRangeAddress(a.getRow(), b.getRow(), a.getCol(), b.getCol()); } + + /** + * Shifts cell range by specified number of rows and columns. + * + * @param rows rows to shift by. + * @param columns columns to shift by. + * @return copy of this {@link CellRangeAddress}, shifted by rows and columns. + * @since 6.0.0 + */ + public CellRangeAddress shift(int rows, int columns) { + return new CellRangeAddress(getFirstRow() + rows, getLastRow() + rows, + getFirstColumn() + columns, getLastColumn() + columns); + } + } diff --git a/poi/src/main/java/org/apache/poi/ss/util/CellRangeAddressBase.java b/poi/src/main/java/org/apache/poi/ss/util/CellRangeAddressBase.java index 7153dff29a..3b2e1e373c 100644 --- a/poi/src/main/java/org/apache/poi/ss/util/CellRangeAddressBase.java +++ b/poi/src/main/java/org/apache/poi/ss/util/CellRangeAddressBase.java @@ -228,6 +228,21 @@ public abstract class CellRangeAddressBase implements Iterable, Dup other._firstCol <= this._lastCol; } + /** + * Determines whether this CellRangeAddress fully contains CellRangeAddress. + * + * @param other a candidate cell range address to check if contained within this range + * @return returns true if this range contains other range. + * @see #isInRange(int, int) for checking if a single cell contains + * @since 6.0.0 + */ + public boolean contains(CellRangeAddressBase other) { + return this._firstRow <= other._firstRow && + this._lastRow >= other._lastRow && + this._firstCol <= other._firstCol && + this._lastCol >= other._lastCol; + } + /** * Useful for logic like table/range styling, where some elements apply based on relative position in a range. * @return set of {@link CellPosition}s occupied by the given coordinates. Empty if the coordinates are not in the range, never null. diff --git a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestRangeCopier.java b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestRangeCopier.java index 6239eb22b5..5ba489385b 100644 --- a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestRangeCopier.java +++ b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestRangeCopier.java @@ -152,9 +152,27 @@ public abstract class BaseTestRangeCopier { transSheetRangeCopier.copyRange(tileRange, tileRange, false, true); assertEquals(cellContent, getCellContent(destSheet, "D6")); assertFalse(destSheet.getMergedRegions().isEmpty()); - destSheet.getMergedRegions().forEach((mergedRegion) -> { - assertEquals(mergedRangeAddress, mergedRegion); - }); + destSheet.getMergedRegions().forEach(mergedRegion -> + assertEquals(mergedRangeAddress, mergedRegion) + ); + } + + @Test + void testSameSheetMergedRanges() { + String cellContent = "D6 merged to E7"; + + // create cell merged from D6 to E7 + CellRangeAddress mergedRangeAddress = new CellRangeAddress(5,6,3,4); + Cell cell = sheet1.createRow(5).createCell(3); + cell.setCellValue(cellContent); + sheet1.addMergedRegion(mergedRangeAddress); + + CellRangeAddress tileRange = CellRangeAddress.valueOf("D6:E7"); + CellRangeAddress targetRange = CellRangeAddress.valueOf("D8:E9"); + rangeCopier.copyRange(tileRange, targetRange, false, true); + assertEquals(cellContent, getCellContent(sheet1, "D8")); + assertFalse(sheet1.getMergedRegions().isEmpty()); + assertTrue(sheet1.getMergedRegions().stream().anyMatch(targetRange::equals)); } protected static String getCellContent(Sheet sheet, String coordinates) {