Skip to content

Comments

feature: support multi-rows loop filling and merge strategies#842

Open
bengbengbalabalabeng wants to merge 6 commits intoapache:mainfrom
bengbengbalabalabeng:feature-issues-836
Open

feature: support multi-rows loop filling and merge strategies#842
bengbengbalabalabeng wants to merge 6 commits intoapache:mainfrom
bengbengbalabalabeng:feature-issues-836

Conversation

@bengbengbalabalabeng
Copy link
Contributor

Purpose of the pull request

Related: #836

What's changed?

1. Support multi-rows loop filling.

Refactor row shifting logic in ExcelWriteFillExecutor. Updated shiftRows and doFill to correctly handle row offsets when a loop item spans multiple rows.

2. Add FillMergeStrategy to handing merge across rows and columns.

Implemented through the addMergedRegionIfNecessary method, 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

  • I have read the Contributor Guide.
  • I have written the necessary doc or comment.
  • I have added the necessary unit tests and all cases have passed.

@bengbengbalabalabeng
Copy link
Contributor Author

@delei PTAL :)

@alaahong alaahong requested a review from Copilot February 21, 2026 06:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ExcelWriteFillExecutor to account for multi-row “rowSpan” when shifting rows and advancing write positions during loop fills.
  • Introduces FillMergeStrategy + FillConfig.mergeStrategy to 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.

Comment on lines +206 to +210

for (AnalysisCell cell : cells) {
for (CellRangeAddress range : sheet.getMergedRegions()) {
if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) {
int firstRow = collectionLastIndexMap.get(cell);
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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())) {

Copilot uses AI. Check for mistakes.
import org.junit.jupiter.api.io.TempDir;

/**
* Tests for multi-rows loop filling, cells merge handing and copy merged cell-styles.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
* 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.

Copilot uses AI. Check for mistakes.
private File caseF03;
private File caseFTemplate03;

private static final int MOCK_DATA_SIZE = 1000;
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
private static final int MOCK_DATA_SIZE = 1000;
private static final int MOCK_DATA_SIZE = 50;

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +147
## 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.

Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +207 to +209
for (AnalysisCell cell : cells) {
for (CellRangeAddress range : sheet.getMergedRegions()) {
if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) {
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +188
// Merge cells
for (CellRangeAddress range : dataRowMergeRegions) {
sheet.addMergedRegionUnsafe(range.copy());
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +38
* 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.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
* 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.

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +134
## 列表填充合并策略

### 概述

在处理列表数据填充时,模板中可能定义了复杂的跨行或跨列合并结构。默认情况下,Fesod 不会自动合并跨行跨列单元格。但是,您可以使用 `mergeStrategy` 参数来控制合并行为。

### 合并策略

- **NONE**: 不进行任何自动合并(默认)。
- **AUTO**: Fesod 会参照模板行的合并结构,自动对生成的每一行数据应用相同的合并区域。
- **MERGE_CELL_STYLE**: 在 `AUTO` 的基础上,将 **锚定单元格(左上角单元格)** 的样式应用到整个合并区域内的所有单元格。
- *注意:过多的单元格样式实例可能导致性能问题,并可能超出单元格样式数量限制(.xlsx 格式为 64000 个,.xls 格式为 4000 个),请在数据量较大时谨慎使用。*
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +211
for (CellRangeAddress range : sheet.getMergedRegions()) {
if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) {
int firstRow = collectionLastIndexMap.get(cell);
int lastRow = firstRow + (range.getLastRow() - range.getFirstRow());
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants