feature: support multi-rows loop filling and merge strategies#842
feature: support multi-rows loop filling and merge strategies#842bengbengbalabalabeng wants to merge 6 commits intoapache:mainfrom
Conversation
- Fix incorrect shiftRows behavior in the combined filling mode - Fix SXSSF row flushing issue where flushed rows become inaccessible - Add corresponding unit tests
|
@delei PTAL :) |
There was a problem hiding this comment.
Pull request overview
Adds support for multi-row loop filling and introduces configurable merged-region replication during vertical list filling, with accompanying docs and regression tests.
Changes:
- Refactors
ExcelWriteFillExecutorto account for multi-row “rowSpan” when shifting rows and advancing write positions during loop fills. - Introduces
FillMergeStrategy+FillConfig.mergeStrategyto control automatic merge replication (and optional merged-cell style unification). - Adds docs (EN + zh-CN) and a comprehensive integration test suite with new/updated fill templates.
Reviewed changes
Copilot reviewed 7 out of 31 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md | Adds zh-CN documentation for merge strategies and multi-row loop filling examples. |
| website/docs/sheet/fill/fill.md | Adds EN documentation for merge strategies and multi-row loop filling examples. |
| fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/fill/FillConfig.java | Adds mergeStrategy to fill configuration with defaults + validation. |
| fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/ExcelWriteFillExecutor.java | Implements multi-row row-span advancing + merge replication and merged-style unification. |
| fesod-sheet/src/main/java/org/apache/fesod/sheet/enums/FillMergeStrategy.java | New enum defining merge handling strategies for loop fills. |
| fesod-sheet/src/test/java/org/apache/fesod/sheet/fill/LoopRowFillingMergeTest.java | New end-to-end tests covering multi-row fill + merge strategies for xls/xlsx and combined fill. |
| fesod-sheet/src/test/java/org/apache/fesod/sheet/fill/LoopRowFillingMergeModel.java | Test model for multi-row fill cases. |
| fesod-sheet/src/test/resources/fill/case_a_07.xlsx | Template for base multi-row loop fill (.xlsx). |
| fesod-sheet/src/test/resources/fill/case_b_07.xlsx | Template for AUTO merge strategy (.xlsx). |
| fesod-sheet/src/test/resources/fill/case_c_07.xlsx | Template for MERGE_CELL_STYLE strategy (.xlsx). |
| fesod-sheet/src/test/resources/fill/case_d_07.xlsx | Template for combined list+common base fill (.xlsx). |
| fesod-sheet/src/test/resources/fill/case_e_07.xlsx | Template for combined list+common fill with AUTO merge (.xlsx). |
| fesod-sheet/src/test/resources/fill/case_f_07.xlsx | Template for combined list+common fill with MERGE_CELL_STYLE (.xlsx). |
| fesod-sheet/src/test/resources/fill/case_a_03.xls | Template for base multi-row loop fill (.xls). |
| fesod-sheet/src/test/resources/fill/case_b_03.xls | Template for AUTO merge strategy (.xls). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| for (AnalysisCell cell : cells) { | ||
| for (CellRangeAddress range : sheet.getMergedRegions()) { | ||
| if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { | ||
| int firstRow = collectionLastIndexMap.get(cell); |
There was a problem hiding this comment.
determineMergedRegionsForRow assumes collectionLastIndexMap is non-null, but addMergedRegionIfNecessary is invoked for non-collection fills as well (the else branch calls doFill(..., rowSpan=0)). If a caller sets mergeStrategy != NONE for a non-collection fill, collectionLastIndexCache.get(currentUniqueDataFlag) will be null and this will throw an NPE. Please guard by returning an empty set when collectionLastIndexMap is null (and/or only applying merge strategies when filling collection data / analysisCell.getCellType() == COLLECTION).
| for (AnalysisCell cell : cells) { | |
| for (CellRangeAddress range : sheet.getMergedRegions()) { | |
| if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { | |
| int firstRow = collectionLastIndexMap.get(cell); | |
| if (collectionLastIndexMap == null || collectionLastIndexMap.isEmpty()) { | |
| return Collections.emptySet(); | |
| } | |
| for (AnalysisCell cell : cells) { | |
| Integer firstRow = collectionLastIndexMap.get(cell); | |
| if (firstRow == null) { | |
| // No recorded last index for this cell; skip to avoid NPE. | |
| continue; | |
| } | |
| for (CellRangeAddress range : sheet.getMergedRegions()) { | |
| if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { |
| import org.junit.jupiter.api.io.TempDir; | ||
|
|
||
| /** | ||
| * Tests for multi-rows loop filling, cells merge handing and copy merged cell-styles. |
There was a problem hiding this comment.
Class-level JavaDoc has typos (“merge handing”). Please correct to “merge handling” (and similarly in any other new/updated comments) so IDE-rendered docs stay professional.
| * Tests for multi-rows loop filling, cells merge handing and copy merged cell-styles. | |
| * Tests for multi-rows loop filling, cells merge handling and copy merged cell-styles. |
| private File caseF03; | ||
| private File caseFTemplate03; | ||
|
|
||
| private static final int MOCK_DATA_SIZE = 1000; |
There was a problem hiding this comment.
MOCK_DATA_SIZE is set to 1000 and the merge validation does repeated linear scans over the merged-region list for each row/column. This can make the unit test noticeably slow/flaky in CI. Consider reducing the dataset size (while still covering multi-row behavior) and/or indexing merged regions (e.g., map from (row,col) to ranges) to keep the test runtime predictable.
| private static final int MOCK_DATA_SIZE = 1000; | |
| private static final int MOCK_DATA_SIZE = 50; |
| ## Multi-Row Loop Filling Merge Strategy | ||
|
|
||
| ### Overview | ||
|
|
||
| When filling list data, the template often contains complex merged regions spanning across rows or | ||
| columns. By default, Fesod fills the data but does not automatically replicate these merged regions. You can use | ||
| the `mergeStrategy` parameter to control the merging behavior of the generated rows. | ||
|
|
There was a problem hiding this comment.
Doc text has a few typos/grammar issues (e.g., “handing merge”, “handing merge across rows and columns”). Please update to “handling” for clarity and professionalism (also consider using ASCII ':' in “Warning:” for consistency).
| for (AnalysisCell cell : cells) { | ||
| for (CellRangeAddress range : sheet.getMergedRegions()) { | ||
| if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { |
There was a problem hiding this comment.
determineMergedRegionsForRow only considers merged regions that intersect cells containing template variables (analysisCellList). Any merged regions within the template row-span that do not cover a placeholder cell will never be replicated, which seems to conflict with the docs/PR description that say the template row’s merge structure is replicated. If the intent is to replicate all merges in the loop block, consider scanning merged regions by row-span bounds rather than by placeholder cells.
| // Merge cells | ||
| for (CellRangeAddress range : dataRowMergeRegions) { | ||
| sheet.addMergedRegionUnsafe(range.copy()); |
There was a problem hiding this comment.
Current merge replication will also re-add the template’s existing merged regions for the first loop item (offset 0), which can create duplicate merged regions in the sheet. It would be safer to skip adding merges when the computed offset is 0 and the merged region already exists, or de-duplicate against existing merged regions before calling addMergedRegionUnsafe.
| // Merge cells | |
| for (CellRangeAddress range : dataRowMergeRegions) { | |
| sheet.addMergedRegionUnsafe(range.copy()); | |
| // Merge cells | |
| // Avoid adding duplicate merged regions by tracking existing ones | |
| Set<String> existingMergedRegionKeys = new HashSet<>(); | |
| for (CellRangeAddress existingRange : sheet.getMergedRegions()) { | |
| String key = existingRange.getFirstRow() | |
| + ":" + existingRange.getLastRow() | |
| + ":" + existingRange.getFirstColumn() | |
| + ":" + existingRange.getLastColumn(); | |
| existingMergedRegionKeys.add(key); | |
| } | |
| for (CellRangeAddress range : dataRowMergeRegions) { | |
| String key = range.getFirstRow() | |
| + ":" + range.getLastRow() | |
| + ":" + range.getFirstColumn() | |
| + ":" + range.getLastColumn(); | |
| if (existingMergedRegionKeys.contains(key)) { | |
| continue; | |
| } | |
| sheet.addMergedRegionUnsafe(range.copy()); | |
| existingMergedRegionKeys.add(key); |
| * Merged regions in the fill template will NOT be handing. | ||
| */ | ||
| NONE, | ||
|
|
||
| /** | ||
| * Automatically handling merged regions base on fill template. | ||
| */ | ||
| AUTO, | ||
|
|
||
| /** | ||
| * Automatically handling merged regions and unify the style using anchor cells base on fill template. |
There was a problem hiding this comment.
The JavaDoc/comment uses “handing” where “handling” is intended (e.g., “Merged regions … will NOT be handing”, “base on”). Please fix wording to avoid confusion in a public enum’s documentation.
| * Merged regions in the fill template will NOT be handing. | |
| */ | |
| NONE, | |
| /** | |
| * Automatically handling merged regions base on fill template. | |
| */ | |
| AUTO, | |
| /** | |
| * Automatically handling merged regions and unify the style using anchor cells base on fill template. | |
| * Merged regions in the fill template will NOT be handled. | |
| */ | |
| NONE, | |
| /** | |
| * Automatically handling merged regions based on the fill template. | |
| */ | |
| AUTO, | |
| /** | |
| * Automatically handling merged regions and unifying the style using anchor cells based on the fill template. |
| ## 列表填充合并策略 | ||
|
|
||
| ### 概述 | ||
|
|
||
| 在处理列表数据填充时,模板中可能定义了复杂的跨行或跨列合并结构。默认情况下,Fesod 不会自动合并跨行跨列单元格。但是,您可以使用 `mergeStrategy` 参数来控制合并行为。 | ||
|
|
||
| ### 合并策略 | ||
|
|
||
| - **NONE**: 不进行任何自动合并(默认)。 | ||
| - **AUTO**: Fesod 会参照模板行的合并结构,自动对生成的每一行数据应用相同的合并区域。 | ||
| - **MERGE_CELL_STYLE**: 在 `AUTO` 的基础上,将 **锚定单元格(左上角单元格)** 的样式应用到整个合并区域内的所有单元格。 | ||
| - *注意:过多的单元格样式实例可能导致性能问题,并可能超出单元格样式数量限制(.xlsx 格式为 64000 个,.xls 格式为 4000 个),请在数据量较大时谨慎使用。* |
There was a problem hiding this comment.
Doc text has a few typos/grammar issues (e.g., “handing merge”, “handing merge across rows and columns”). Please update to “处理/处理合并” (“handling”) in the Chinese doc to keep terminology consistent.
| for (CellRangeAddress range : sheet.getMergedRegions()) { | ||
| if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { | ||
| int firstRow = collectionLastIndexMap.get(cell); | ||
| int lastRow = firstRow + (range.getLastRow() - range.getFirstRow()); |
There was a problem hiding this comment.
determineMergedRegionsForRow calculates the replicated merged range using firstRow = collectionLastIndexMap.get(cell). This only works if the template variable cell is on the merged region’s first row; if a placeholder exists in any non-anchor cell inside a merged region, the computed firstRow/lastRow will be wrong and can create overlapping/incorrect merges. Consider computing a row offset (collectionLastIndexMap.get(cell) - cell.getRowIndex()) and shifting the original range by that offset (and/or only using the merged region’s anchor cell) to generate the replicated CellRangeAddress.
| for (CellRangeAddress range : sheet.getMergedRegions()) { | |
| if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { | |
| int firstRow = collectionLastIndexMap.get(cell); | |
| int lastRow = firstRow + (range.getLastRow() - range.getFirstRow()); | |
| Integer mappedRowIndex = collectionLastIndexMap.get(cell); | |
| if (mappedRowIndex == null) { | |
| continue; | |
| } | |
| int rowOffset = mappedRowIndex - cell.getRowIndex(); | |
| for (CellRangeAddress range : sheet.getMergedRegions()) { | |
| if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { | |
| int firstRow = range.getFirstRow() + rowOffset; | |
| int lastRow = range.getLastRow() + rowOffset; |
Purpose of the pull request
Related: #836
What's changed?
1. Support multi-rows loop filling.
Refactor row shifting logic in
ExcelWriteFillExecutor. UpdatedshiftRowsanddoFillto correctly handle row offsets when a loop item spans multiple rows.2. Add
FillMergeStrategyto handing merge across rows and columns.Implemented through the
addMergedRegionIfNecessarymethod, which replicates the merge structure defined in the template row and applies it to each generated data row.See the API Usage Example for usage examples.
Checklist