This is an automated email from the ASF dual-hosted git repository.

delei pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fesod.git


The following commit(s) were added to refs/heads/main by this push:
     new a501139b feat: introduce header merge strategy for Excel writing (#674)
a501139b is described below

commit a501139b4d876e039df12afedfc88fcded3654b6
Author: Guangdong Liu <[email protected]>
AuthorDate: Tue Dec 9 09:49:28 2025 +0800

    feat: introduce header merge strategy for Excel writing (#674)
    
    * feat: introduce header merge strategy for Excel writing
    
    * feat: enhance header merge strategy with context validation and improved 
documentation
    
    * feat: enhance header merge strategy with context validation and improved 
documentation
    
    * feat: introduce HeaderMergeStrategy for flexible header merging and fix 
issue #666
    
    * feat: introduce HeaderMergeStrategy for flexible header merging and fix 
issue #666
    
    * Update 
fesod/src/test/java/org/apache/fesod/excel/head/HeaderMergeStrategyTest.java
    
    Co-authored-by: Copilot <[email protected]>
    
    * Update 
fesod/src/test/java/org/apache/fesod/excel/head/HeaderMergeStrategyTest.java
    
    Co-authored-by: Copilot <[email protected]>
    
    * feat: simplify canMergeVertically method by removing unused parameter
    
    * doc: add line breaks for better readability in parameter documentation
    
    ---------
    
    Co-authored-by: DeleiGuo <[email protected]>
    Co-authored-by: Copilot <[email protected]>
    Co-authored-by: Shuxin Pan <[email protected]>
    Co-authored-by: gdliu3 <[email protected]>
---
 .../fesod/sheet/context/WriteContextImpl.java      |  12 +-
 .../fesod/sheet/enums/HeaderMergeStrategy.java     |  58 ++++++
 .../AbstractExcelWriterParameterBuilder.java       |  13 ++
 .../sheet/write/metadata/WriteBasicParameter.java  |   6 +
 .../write/metadata/holder/AbstractWriteHolder.java |  26 +++
 .../sheet/write/metadata/holder/WriteHolder.java   |   9 +
 .../write/property/ExcelWriteHeadProperty.java     | 170 ++++++++++++++---
 .../fesod/sheet/head/HeaderMergeStrategyTest.java  | 210 +++++++++++++++++++++
 website/docs/sheet/help/parameter.md               |  27 +++
 website/docs/sheet/write/head.md                   |  50 +++++
 .../current/sheet/help/parameter.md                |  27 +++
 .../current/sheet/write/head.md                    |  50 +++++
 12 files changed, 632 insertions(+), 26 deletions(-)

diff --git 
a/fesod/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java 
b/fesod/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java
index 92fa55da..aae38a62 100644
--- a/fesod/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java
+++ b/fesod/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java
@@ -25,6 +25,7 @@ import java.io.OutputStream;
 import java.util.Map;
 import java.util.UUID;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.fesod.sheet.enums.HeaderMergeStrategy;
 import org.apache.fesod.sheet.enums.WriteTypeEnum;
 import org.apache.fesod.sheet.exception.ExcelGenerateException;
 import org.apache.fesod.sheet.metadata.CellRange;
@@ -296,8 +297,9 @@ public class WriteContextImpl implements WriteContext {
         }
         int newRowIndex = writeSheetHolder.getNewRowIndexAndStartDoWrite();
         newRowIndex += currentWriteHolder.relativeHeadRowIndex();
-        if (currentWriteHolder.automaticMergeHead()) {
-            addMergedRegionToCurrentSheet(excelWriteHeadProperty, newRowIndex);
+        HeaderMergeStrategy mergeStrategy = 
currentWriteHolder.headerMergeStrategy();
+        if (mergeStrategy != null && mergeStrategy != 
HeaderMergeStrategy.NONE) {
+            addMergedRegionToCurrentSheet(excelWriteHeadProperty, newRowIndex, 
mergeStrategy);
         }
         for (int relativeRowIndex = 0, i = newRowIndex;
                 i < excelWriteHeadProperty.getHeadRowNumber() + newRowIndex;
@@ -321,9 +323,11 @@ public class WriteContextImpl implements WriteContext {
      *
      * @param excelWriteHeadProperty The header property for writing.
      * @param rowIndex               The starting row index for merging.
+     * @param mergeStrategy         The merge strategy to use.
      */
-    private void addMergedRegionToCurrentSheet(ExcelWriteHeadProperty 
excelWriteHeadProperty, int rowIndex) {
-        for (CellRange cellRangeModel : 
excelWriteHeadProperty.headCellRangeList()) {
+    private void addMergedRegionToCurrentSheet(
+            ExcelWriteHeadProperty excelWriteHeadProperty, int rowIndex, 
HeaderMergeStrategy mergeStrategy) {
+        for (CellRange cellRangeModel : 
excelWriteHeadProperty.headCellRangeList(mergeStrategy)) {
             writeSheetHolder
                     .getSheet()
                     .addMergedRegionUnsafe(new CellRangeAddress(
diff --git 
a/fesod/src/main/java/org/apache/fesod/sheet/enums/HeaderMergeStrategy.java 
b/fesod/src/main/java/org/apache/fesod/sheet/enums/HeaderMergeStrategy.java
new file mode 100644
index 00000000..111bfb2e
--- /dev/null
+++ b/fesod/src/main/java/org/apache/fesod/sheet/enums/HeaderMergeStrategy.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.enums;
+
+/**
+ * Header merge strategy for Excel writing.
+ * <p>
+ * When {@code headerMergeStrategy} is not set (null), the behavior is 
determined by
+ * {@code automaticMergeHead} for backward compatibility:
+ * {@code automaticMergeHead == true} → {@code AUTO}, {@code 
automaticMergeHead == false} → {@code NONE}.
+ * </p>
+ *
+ * @see 
org.apache.fesod.excel.write.metadata.WriteBasicParameter#getHeaderMergeStrategy()
+ * @see 
org.apache.fesod.excel.write.builder.AbstractExcelWriterParameterBuilder#headerMergeStrategy(HeaderMergeStrategy)
+ */
+public enum HeaderMergeStrategy {
+    /**
+     * No automatic merge
+     */
+    NONE,
+
+    /**
+     * Only horizontal merge (same cells in the same row)
+     */
+    HORIZONTAL_ONLY,
+
+    /**
+     * Only vertical merge (same cells in the same column).
+     */
+    VERTICAL_ONLY,
+
+    /**
+     * Only full rectangle merge (all cells must form a complete rectangle 
with the same name)
+     */
+    FULL_RECTANGLE,
+
+    /**
+     * Auto merge (default behavior for backward compatibility).
+     */
+    AUTO
+}
diff --git 
a/fesod/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java
 
b/fesod/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java
index c30215ac..e2ed55dd 100644
--- 
a/fesod/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java
+++ 
b/fesod/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java
@@ -21,6 +21,7 @@ package org.apache.fesod.sheet.write.builder;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import org.apache.fesod.sheet.enums.HeaderMergeStrategy;
 import org.apache.fesod.sheet.metadata.AbstractParameterBuilder;
 import org.apache.fesod.sheet.write.handler.WriteHandler;
 import org.apache.fesod.sheet.write.metadata.WriteBasicParameter;
@@ -88,6 +89,18 @@ public abstract class AbstractExcelWriterParameterBuilder<
         return self();
     }
 
+    /**
+     * Set header merge strategy.
+     * If not set, the behavior is determined by {@link #automaticMergeHead} 
for backward compatibility.
+     *
+     * @param strategy Header merge strategy
+     * @return this
+     */
+    public T headerMergeStrategy(HeaderMergeStrategy strategy) {
+        parameter().setHeaderMergeStrategy(strategy);
+        return self();
+    }
+
     /**
      * Ignore the custom columns.
      */
diff --git 
a/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java
 
b/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java
index 5ce6f211..c29cc477 100644
--- 
a/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java
+++ 
b/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java
@@ -25,6 +25,7 @@ import java.util.List;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
+import org.apache.fesod.sheet.enums.HeaderMergeStrategy;
 import org.apache.fesod.sheet.metadata.BasicParameter;
 import org.apache.fesod.sheet.write.handler.WriteHandler;
 
@@ -57,6 +58,11 @@ public class WriteBasicParameter extends BasicParameter {
      * Whether to automatically merge headers.Default is true.
      */
     private Boolean automaticMergeHead;
+    /**
+     * Header merge strategy.
+     * If null, the behavior is determined by {@link #automaticMergeHead} for 
backward compatibility.
+     */
+    private HeaderMergeStrategy headerMergeStrategy;
     /**
      * Ignore the custom columns.
      */
diff --git 
a/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java
 
b/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java
index 14031c46..eae8c3fd 100644
--- 
a/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java
+++ 
b/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java
@@ -37,6 +37,7 @@ import org.apache.fesod.sheet.converters.Converter;
 import org.apache.fesod.sheet.converters.ConverterKeyBuild;
 import org.apache.fesod.sheet.converters.DefaultConverterLoader;
 import org.apache.fesod.sheet.enums.HeadKindEnum;
+import org.apache.fesod.sheet.enums.HeaderMergeStrategy;
 import org.apache.fesod.sheet.event.NotRepeatExecutor;
 import org.apache.fesod.sheet.metadata.AbstractHolder;
 import org.apache.fesod.sheet.metadata.Head;
@@ -92,6 +93,11 @@ public abstract class AbstractWriteHolder extends 
AbstractHolder implements Writ
      * Whether to automatically merge headers.Default is true.
      */
     private Boolean automaticMergeHead;
+    /**
+     * Header merge strategy.
+     * If null, the behavior is determined by {@link #automaticMergeHead} for 
backward compatibility.
+     */
+    private HeaderMergeStrategy headerMergeStrategy;
 
     /**
      * Ignore the custom columns.
@@ -201,6 +207,17 @@ public abstract class AbstractWriteHolder extends 
AbstractHolder implements Writ
             this.automaticMergeHead = 
writeBasicParameter.getAutomaticMergeHead();
         }
 
+        if (writeBasicParameter.getHeaderMergeStrategy() == null) {
+            if (parentAbstractWriteHolder == null) {
+                // Backward compatibility: if headerMergeStrategy is not set, 
use automaticMergeHead
+                this.headerMergeStrategy = null;
+            } else {
+                this.headerMergeStrategy = 
parentAbstractWriteHolder.getHeaderMergeStrategy();
+            }
+        } else {
+            this.headerMergeStrategy = 
writeBasicParameter.getHeaderMergeStrategy();
+        }
+
         if (writeBasicParameter.getExcludeColumnFieldNames() == null && 
parentAbstractWriteHolder != null) {
             this.excludeColumnFieldNames = 
parentAbstractWriteHolder.getExcludeColumnFieldNames();
         } else {
@@ -517,6 +534,15 @@ public abstract class AbstractWriteHolder extends 
AbstractHolder implements Writ
         return getAutomaticMergeHead();
     }
 
+    @Override
+    public HeaderMergeStrategy headerMergeStrategy() {
+        // Backward compatibility: if headerMergeStrategy is null, determine 
based on automaticMergeHead
+        if (headerMergeStrategy == null) {
+            return automaticMergeHead ? HeaderMergeStrategy.AUTO : 
HeaderMergeStrategy.NONE;
+        }
+        return headerMergeStrategy;
+    }
+
     @Override
     public boolean orderByIncludeColumn() {
         return getOrderByIncludeColumn();
diff --git 
a/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteHolder.java
 
b/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteHolder.java
index a889f8cb..b76ae88b 100644
--- 
a/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteHolder.java
+++ 
b/fesod/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteHolder.java
@@ -20,6 +20,7 @@
 package org.apache.fesod.sheet.write.metadata.holder;
 
 import java.util.Collection;
+import org.apache.fesod.sheet.enums.HeaderMergeStrategy;
 import org.apache.fesod.sheet.metadata.ConfigurationHolder;
 import org.apache.fesod.sheet.write.property.ExcelWriteHeadProperty;
 
@@ -59,6 +60,14 @@ public interface WriteHolder extends ConfigurationHolder {
      */
     boolean automaticMergeHead();
 
+    /**
+     * Get header merge strategy.
+     * If null, the behavior is determined by {@link #automaticMergeHead()} 
for backward compatibility.
+     *
+     * @return Header merge strategy
+     */
+    HeaderMergeStrategy headerMergeStrategy();
+
     /**
      * Writes the head relative to the existing contents of the sheet. Indexes 
are zero-based.
      *
diff --git 
a/fesod/src/main/java/org/apache/fesod/sheet/write/property/ExcelWriteHeadProperty.java
 
b/fesod/src/main/java/org/apache/fesod/sheet/write/property/ExcelWriteHeadProperty.java
index 1b0a621c..4bf2e673 100644
--- 
a/fesod/src/main/java/org/apache/fesod/sheet/write/property/ExcelWriteHeadProperty.java
+++ 
b/fesod/src/main/java/org/apache/fesod/sheet/write/property/ExcelWriteHeadProperty.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -36,6 +37,7 @@ import 
org.apache.fesod.sheet.annotation.write.style.HeadRowHeight;
 import org.apache.fesod.sheet.annotation.write.style.HeadStyle;
 import org.apache.fesod.sheet.annotation.write.style.OnceAbsoluteMerge;
 import org.apache.fesod.sheet.enums.HeadKindEnum;
+import org.apache.fesod.sheet.enums.HeaderMergeStrategy;
 import org.apache.fesod.sheet.metadata.CellRange;
 import org.apache.fesod.sheet.metadata.ConfigurationHolder;
 import org.apache.fesod.sheet.metadata.Head;
@@ -109,11 +111,28 @@ public class ExcelWriteHeadProperty extends 
ExcelHeadProperty {
      * Calculate all cells that need to be merged
      *
      * @return cells that need to be merged
+     * @deprecated Use {@link #headCellRangeList(HeaderMergeStrategy)} instead
      */
+    @Deprecated
     public List<CellRange> headCellRangeList() {
-        List<CellRange> cellRangeList = new ArrayList<CellRange>();
-        Set<String> alreadyRangeSet = new HashSet<String>();
-        List<Head> headList = new ArrayList<Head>(getHeadMap().values());
+        return headCellRangeList(HeaderMergeStrategy.AUTO);
+    }
+
+    /**
+     * Calculate all cells that need to be merged based on the merge strategy
+     *
+     * @param mergeStrategy The merge strategy to use
+     * @return cells that need to be merged
+     */
+    public List<CellRange> headCellRangeList(HeaderMergeStrategy 
mergeStrategy) {
+        if (mergeStrategy == null || mergeStrategy == 
HeaderMergeStrategy.NONE) {
+            return new ArrayList<>();
+        }
+
+        List<CellRange> cellRangeList = new ArrayList<>();
+        Set<String> alreadyRangeSet = new HashSet<>();
+        List<Head> headList = new ArrayList<>(getHeadMap().values());
+
         for (int i = 0; i < headList.size(); i++) {
             Head head = headList.get(i);
             List<String> headNameList = head.getHeadNameList();
@@ -125,37 +144,144 @@ public class ExcelWriteHeadProperty extends 
ExcelHeadProperty {
                 String headName = headNameList.get(j);
                 int lastCol = i;
                 int lastRow = j;
-                for (int k = i + 1; k < headList.size(); k++) {
-                    String key = k + "-" + j;
-                    if 
(headList.get(k).getHeadNameList().get(j).equals(headName) && 
!alreadyRangeSet.contains(key)) {
-                        alreadyRangeSet.add(key);
-                        lastCol = k;
-                    } else {
-                        break;
+
+                // Horizontal merge (if allowed by strategy)
+                if (mergeStrategy == HeaderMergeStrategy.HORIZONTAL_ONLY
+                        || mergeStrategy == HeaderMergeStrategy.FULL_RECTANGLE
+                        || mergeStrategy == HeaderMergeStrategy.AUTO) {
+                    for (int k = i + 1; k < headList.size(); k++) {
+                        String key = k + "-" + j;
+                        if (headList.get(k).getHeadNameList().size() > j
+                                && Objects.equals(
+                                        
headList.get(k).getHeadNameList().get(j), headName)
+                                && !alreadyRangeSet.contains(key)) {
+                            alreadyRangeSet.add(key);
+                            lastCol = k;
+                        } else {
+                            break;
+                        }
                     }
                 }
+
+                // Vertical merge (if allowed by strategy)
                 Set<String> tempAlreadyRangeSet = new HashSet<>();
-                outer:
-                for (int k = j + 1; k < headNameList.size(); k++) {
-                    for (int l = i; l <= lastCol; l++) {
-                        String key = l + "-" + k;
-                        if 
(headList.get(l).getHeadNameList().get(k).equals(headName)
-                                && !alreadyRangeSet.contains(key)) {
-                            tempAlreadyRangeSet.add(l + "-" + k);
+                if (mergeStrategy == HeaderMergeStrategy.VERTICAL_ONLY
+                        || mergeStrategy == HeaderMergeStrategy.FULL_RECTANGLE
+                        || mergeStrategy == HeaderMergeStrategy.AUTO) {
+                    outer:
+                    for (int k = j + 1; k < headNameList.size(); k++) {
+                        // For FULL_RECTANGLE and AUTO, verify all cells in 
the row
+                        boolean canMerge = true;
+                        for (int l = i; l <= lastCol; l++) {
+                            String key = l + "-" + k;
+                            if (headList.get(l).getHeadNameList().size() <= k
+                                    || !Objects.equals(
+                                            
headList.get(l).getHeadNameList().get(k), headName)
+                                    || alreadyRangeSet.contains(key)) {
+                                canMerge = false;
+                                break;
+                            }
+                        }
+
+                        // For AUTO strategy, also check context consistency
+                        if (canMerge && mergeStrategy == 
HeaderMergeStrategy.AUTO) {
+                            canMerge = canMergeVertically(headList, j, k, i, 
lastCol);
+                        }
+
+                        if (canMerge) {
+                            for (int l = i; l <= lastCol; l++) {
+                                String key = l + "-" + k;
+                                tempAlreadyRangeSet.add(key);
+                            }
+                            lastRow = k;
                         } else {
                             break outer;
                         }
                     }
-                    lastRow = k;
                     alreadyRangeSet.addAll(tempAlreadyRangeSet);
                 }
-                if (j == lastRow && i == lastCol) {
-                    continue;
+
+                // For FULL_RECTANGLE strategy, verify the entire rectangle is 
valid
+                if (mergeStrategy == HeaderMergeStrategy.FULL_RECTANGLE) {
+                    if (!isValidRectangleRegion(headList, j, lastRow, i, 
lastCol, headName)) {
+                        // If rectangle is invalid, fall back to single cell 
(no merge)
+                        continue;
+                    }
+                }
+
+                // Add merge range if it's larger than a single cell
+                if (j != lastRow || i != lastCol) {
+                    cellRangeList.add(new CellRange(
+                            j,
+                            lastRow,
+                            head.getColumnIndex(),
+                            headList.get(lastCol).getColumnIndex()));
                 }
-                cellRangeList.add(new CellRange(
-                        j, lastRow, head.getColumnIndex(), 
headList.get(lastCol).getColumnIndex()));
             }
         }
         return cellRangeList;
     }
+
+    /**
+     * Check if two rows can be merged vertically based on context consistency
+     *
+     * @param headList    The list of heads
+     * @param row1        First row index
+     * @param row2        Second row index
+     * @param startCol    Start column index
+     * @param endCol      End column index
+     * @return true if the rows can be merged
+     */
+    private boolean canMergeVertically(List<Head> headList, int row1, int 
row2, int startCol, int endCol) {
+        // Check if there's a row above that provides context
+        if (row1 > 0) {
+            // Check if all cells in the range have the same context above
+            for (int col = startCol; col <= endCol; col++) {
+                boolean hasUpper1 = headList.get(col).getHeadNameList().size() 
> row1;
+                boolean hasUpper2 = headList.get(col).getHeadNameList().size() 
> row2;
+
+                // If one row has upper context but the other doesn't, don't 
merge
+                if (hasUpper1 != hasUpper2) {
+                    return false;
+                }
+
+                if (hasUpper1) {
+                    String upper1 = 
headList.get(col).getHeadNameList().get(row1 - 1);
+                    String upper2 = 
headList.get(col).getHeadNameList().get(row2 - 1);
+                    // If context (upper cells) is different, don't merge
+                    if (!Objects.equals(upper1, upper2)) {
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Verify if a rectangle region is valid (all cells exist and have the 
same name)
+     *
+     * @param headList      The list of heads
+     * @param startRow      Start row index
+     * @param endRow        End row index
+     * @param startCol      Start column index
+     * @param endCol        End column index
+     * @param expectedName  Expected cell name
+     * @return true if the rectangle is valid
+     */
+    private boolean isValidRectangleRegion(
+            List<Head> headList, int startRow, int endRow, int startCol, int 
endCol, String expectedName) {
+        for (int row = startRow; row <= endRow; row++) {
+            for (int col = startCol; col <= endCol; col++) {
+                if (headList.get(col).getHeadNameList().size() <= row) {
+                    return false; // Cell doesn't exist
+                }
+                String cellName = headList.get(col).getHeadNameList().get(row);
+                if (!Objects.equals(expectedName, cellName)) {
+                    return false; // Cell name doesn't match
+                }
+            }
+        }
+        return true;
+    }
 }
diff --git 
a/fesod/src/test/java/org/apache/fesod/sheet/head/HeaderMergeStrategyTest.java 
b/fesod/src/test/java/org/apache/fesod/sheet/head/HeaderMergeStrategyTest.java
new file mode 100644
index 00000000..1dfc16cb
--- /dev/null
+++ 
b/fesod/src/test/java/org/apache/fesod/sheet/head/HeaderMergeStrategyTest.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.head;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.fesod.sheet.FastExcel;
+import org.apache.fesod.sheet.enums.HeaderMergeStrategy;
+import org.apache.fesod.sheet.util.TestFileUtil;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+/**
+ * Test for header merge strategy
+ *
+ */
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class HeaderMergeStrategyTest {
+
+    private static File fileNone;
+    private static File fileHorizontalOnly;
+    private static File fileVerticalOnly;
+    private static File fileFullRectangle;
+    private static File fileAuto;
+
+    @BeforeAll
+    public static void init() {
+        fileNone = TestFileUtil.createNewFile("headerMergeStrategyNone.xlsx");
+        fileHorizontalOnly = 
TestFileUtil.createNewFile("headerMergeStrategyHorizontalOnly.xlsx");
+        fileVerticalOnly = 
TestFileUtil.createNewFile("headerMergeStrategyVerticalOnly.xlsx");
+        fileFullRectangle = 
TestFileUtil.createNewFile("headerMergeStrategyFullRectangle.xlsx");
+        fileAuto = TestFileUtil.createNewFile("headerMergeStrategyAuto.xlsx");
+    }
+
+    @Test
+    public void testNoneStrategy() {
+        List<List<String>> head = createTestHead();
+        FastExcel.write(fileNone)
+                .head(head)
+                .headerMergeStrategy(HeaderMergeStrategy.NONE)
+                .sheet()
+                .doWrite(createTestData());
+
+        // Verify no merged regions
+        try (org.apache.poi.ss.usermodel.Workbook workbook =
+                org.apache.poi.ss.usermodel.WorkbookFactory.create(fileNone)) {
+            Sheet sheet = workbook.getSheetAt(0);
+            Assertions.assertEquals(
+                    0, sheet.getNumMergedRegions(), "NONE strategy should not 
create any merged regions");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to verify merged regions", e);
+        }
+    }
+
+    @Test
+    public void testHorizontalOnlyStrategy() {
+        List<List<String>> head = createTestHead();
+        FastExcel.write(fileHorizontalOnly)
+                .head(head)
+                .headerMergeStrategy(HeaderMergeStrategy.HORIZONTAL_ONLY)
+                .sheet()
+                .doWrite(createTestData());
+
+        // Verify only horizontal merges exist
+        try (org.apache.poi.ss.usermodel.Workbook workbook =
+                
org.apache.poi.ss.usermodel.WorkbookFactory.create(fileHorizontalOnly)) {
+            Sheet sheet = workbook.getSheetAt(0);
+            int mergedRegionCount = sheet.getNumMergedRegions();
+
+            // All merged regions should be horizontal only (same row)
+            for (int i = 0; i < mergedRegionCount; i++) {
+                CellRangeAddress region = sheet.getMergedRegion(i);
+                Assertions.assertEquals(
+                        region.getFirstRow(),
+                        region.getLastRow(),
+                        "HORIZONTAL_ONLY strategy should only merge cells in 
the same row");
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to verify merged regions", e);
+        }
+    }
+
+    @Test
+    public void testVerticalOnlyStrategy() {
+        List<List<String>> head = createTestHead();
+        FastExcel.write(fileVerticalOnly)
+                .head(head)
+                .headerMergeStrategy(HeaderMergeStrategy.VERTICAL_ONLY)
+                .sheet()
+                .doWrite(createTestData());
+
+        // Verify only vertical merges exist
+        try (org.apache.poi.ss.usermodel.Workbook workbook =
+                
org.apache.poi.ss.usermodel.WorkbookFactory.create(fileVerticalOnly)) {
+            Sheet sheet = workbook.getSheetAt(0);
+            int mergedRegionCount = sheet.getNumMergedRegions();
+
+            // All merged regions should be vertical only (same column)
+            for (int i = 0; i < mergedRegionCount; i++) {
+                CellRangeAddress region = sheet.getMergedRegion(i);
+                Assertions.assertEquals(
+                        region.getFirstColumn(),
+                        region.getLastColumn(),
+                        "VERTICAL_ONLY strategy should only merge cells in the 
same column");
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to verify merged regions", e);
+        }
+    }
+
+    @Test
+    public void testFullRectangleStrategy() {
+        List<List<String>> head = createTestHead();
+        FastExcel.write(fileFullRectangle)
+                .head(head)
+                .headerMergeStrategy(HeaderMergeStrategy.FULL_RECTANGLE)
+                .sheet()
+                .doWrite(createTestData());
+
+        // Verify all merged regions form valid rectangles
+        try (org.apache.poi.ss.usermodel.Workbook workbook =
+                
org.apache.poi.ss.usermodel.WorkbookFactory.create(fileFullRectangle)) {
+            Sheet sheet = workbook.getSheetAt(0);
+            int mergedRegionCount = sheet.getNumMergedRegions();
+
+            // All merged regions should be valid rectangles
+            for (int i = 0; i < mergedRegionCount; i++) {
+                CellRangeAddress region = sheet.getMergedRegion(i);
+                // Verify rectangle is valid (not just a single cell)
+                Assertions.assertTrue(
+                        region.getFirstRow() != region.getLastRow()
+                                || region.getFirstColumn() != 
region.getLastColumn(),
+                        "Merged region should not be a single cell");
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to verify merged regions", e);
+        }
+    }
+
+    @Test
+    public void testAutoStrategy() {
+        List<List<String>> head = createTestHead();
+        FastExcel.write(fileAuto)
+                .head(head)
+                .headerMergeStrategy(HeaderMergeStrategy.AUTO)
+                .sheet()
+                .doWrite(createTestData());
+
+        // AUTO strategy should work similar to the old behavior
+        try (org.apache.poi.ss.usermodel.Workbook workbook =
+                org.apache.poi.ss.usermodel.WorkbookFactory.create(fileAuto)) {
+            Sheet sheet = workbook.getSheetAt(0);
+            // Just verify that the file was created successfully
+            Assertions.assertNotNull(sheet, "Sheet should be created");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to verify merged regions", e);
+        }
+    }
+
+    /**
+     * Create test head data with mergeable cells
+     */
+    private List<List<String>> createTestHead() {
+        List<List<String>> head = new ArrayList<>();
+        // Columns 0-2 with row 0: ["A"], ["A"], ["B"]
+        head.add(new ArrayList<>(Arrays.asList("A")));
+        head.add(new ArrayList<>(Arrays.asList("A")));
+        head.add(new ArrayList<>(Arrays.asList("B")));
+        // Columns 0-2 with row 0 and row 1: ["A", "A1"], ["A", "A2"], ["B", 
"B1"]
+        head.add(new ArrayList<>(Arrays.asList("A", "A1")));
+        head.add(new ArrayList<>(Arrays.asList("A", "A2")));
+        head.add(new ArrayList<>(Arrays.asList("B", "B1")));
+        return head;
+    }
+
+    /**
+     * Create test data
+     */
+    private List<List<Object>> createTestData() {
+        List<List<Object>> data = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            data.add(new ArrayList<>(Arrays.asList("A" + i, "B" + i, "C" + i, 
"D" + i, "E" + i, "F" + i)));
+        }
+        return data;
+    }
+}
diff --git a/website/docs/sheet/help/parameter.md 
b/website/docs/sheet/help/parameter.md
index 1926d0fe..58d85a2b 100644
--- a/website/docs/sheet/help/parameter.md
+++ b/website/docs/sheet/help/parameter.md
@@ -56,6 +56,7 @@ class ReadWorkbook {
 class WriteBasicParameter {
   - Boolean useDefaultStyle
   - Boolean automaticMergeHead
+  - HeaderMergeStrategy headerMergeStrategy
   - Collection~Integer~ includeColumnIndexes
   - Collection~String~ excludeColumnFieldNames
   - Boolean orderByIncludeColumn
@@ -165,12 +166,38 @@ All parameters inherit from `BasicParameter`.
 | needHead                | true          | Whether to write the header to 
spreadsheet.                                                                    
                                                                        |
 | useDefaultStyle         | true          | Whether to use default styles.     
                                                                                
                                                                    |
 | automaticMergeHead      | true          | Automatically merge headers, 
matching the same fields above, below, left, and right in the header.           
                                                                          |
+| headerMergeStrategy     | null          | Header merge strategy. If null, 
the behavior is determined by `automaticMergeHead` for backward compatibility. 
Options: `NONE`, `HORIZONTAL_ONLY`, `VERTICAL_ONLY`, `FULL_RECTANGLE`, `AUTO`. 
See details below. |
 | excludeColumnIndexes    | Empty         | Exclude indexes of data in the 
object.                                                                         
                                                                        |
 | excludeColumnFieldNames | Empty         | Exclude fields of data in the 
object.                                                                         
                                                                         |
 | includeColumnIndexes    | Empty         | Only export indexes of data in the 
object.                                                                         
                                                                    |
 | includeColumnFieldNames | Empty         | Only export fields of data in the 
object.                                                                         
                                                                     |
 | orderByIncludeColumn    | false         | When using the parameters 
`includeColumnFieldNames` or `includeColumnIndexes`, it will sort according to 
the order of the collection passed in.                                        |
 
+#### Header Merge Strategy
+
+The `headerMergeStrategy` parameter provides fine-grained control over how 
headers are merged:
+
+- **NONE**: No automatic merging is performed.
+- **HORIZONTAL_ONLY**: Only merges cells horizontally (same row).
+- **VERTICAL_ONLY**: Only merges cells vertically (same column).
+- **FULL_RECTANGLE**: Only merges complete rectangular regions where all cells 
have the same name.
+- **AUTO**: Automatic merging (default behavior for backward compatibility).
+
+**Example**:
+
+```java
+FastExcel.write(fileName)
+    .head(head)
+    .headerMergeStrategy(HeaderMergeStrategy.FULL_RECTANGLE)
+    .sheet()
+    .doWrite(data());
+```
+
+**Note**: If `headerMergeStrategy` is not set, the behavior is determined by 
`automaticMergeHead`:
+
+- `automaticMergeHead == true` → `HeaderMergeStrategy.AUTO`
+- `automaticMergeHead == false` → `HeaderMergeStrategy.NONE`
+
 ### WriteWorkbook
 
 | Name                    | Default Value          | Description               
                                                                                
                                    |
diff --git a/website/docs/sheet/write/head.md b/website/docs/sheet/write/head.md
index b25dd43a..f73b83f9 100644
--- a/website/docs/sheet/write/head.md
+++ b/website/docs/sheet/write/head.md
@@ -78,3 +78,53 @@ public void dynamicHeadWrite() {
 ### Result
 
 ![img](/img/docs/write/dynamicHeadWrite.png)
+
+---
+
+## Header Merge Strategy
+
+### Overview
+
+By default, FastExcel automatically merges header cells with the same name. 
However, you can control the merge behavior using the `headerMergeStrategy` 
parameter.
+
+### Merge Strategies
+
+- **NONE**: No automatic merging is performed.
+- **HORIZONTAL_ONLY**: Only merges cells horizontally (same row).
+- **VERTICAL_ONLY**: Only merges cells vertically (same column).
+- **FULL_RECTANGLE**: Only merges complete rectangular regions where all cells 
have the same name.
+- **AUTO**: Automatic merging (default).
+
+### Code Example
+
+```java
+@Test
+public void dynamicHeadWriteWithStrategy() {
+    String fileName = "dynamicHeadWrite" + System.currentTimeMillis() + 
".xlsx";
+
+    List<List<String>> head = Arrays.asList(
+        Collections.singletonList("动态字符串标题"),
+        Collections.singletonList("动态数字标题"),
+        Collections.singletonList("动态日期标题"));
+
+    FastExcel.write(fileName)
+        .head(head)
+        .headerMergeStrategy(HeaderMergeStrategy.FULL_RECTANGLE)
+        .sheet()
+        .doWrite(data());
+}
+```
+
+### Common Use Cases
+
+**Disable merging**: Use `NONE` to completely disable automatic merging:
+
+```java
+FastExcel.write(fileName)
+    .head(head)
+    .headerMergeStrategy(HeaderMergeStrategy.NONE)
+    .sheet()
+    .doWrite(data());
+```
+
+**Note**: The old `automaticMergeHead` parameter is still supported for 
backward compatibility. When `headerMergeStrategy` is not set, the behavior is 
determined by `automaticMergeHead`.
diff --git 
a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/parameter.md
 
b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/parameter.md
index 9badc0e0..50c7eed6 100644
--- 
a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/parameter.md
+++ 
b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/help/parameter.md
@@ -56,6 +56,7 @@ class ReadWorkbook {
 class WriteBasicParameter {
   - Boolean useDefaultStyle
   - Boolean automaticMergeHead
+  - HeaderMergeStrategy headerMergeStrategy
   - Collection~Integer~ includeColumnIndexes
   - Collection~String~ excludeColumnFieldNames
   - Boolean orderByIncludeColumn
@@ -165,12 +166,38 @@ WriteWorkbook  --|>  WriteBasicParameter
 | needHead                | true  | 是否需要写入头到电子表格                               
                                                                |
 | useDefaultStyle         | true  | 是否使用默认的样式                                  
                                                                |
 | automaticMergeHead      | true  | 自动合并头,头中相同的字段上下左右都会去尝试匹配                   
                                                                |
+| headerMergeStrategy     | null  | 表头合并策略。如果为 null,则根据 `automaticMergeHead` 
决定行为以保持向后兼容。可选值:`NONE`、`HORIZONTAL_ONLY`、`VERTICAL_ONLY`、`FULL_RECTANGLE`、`AUTO`。详见下方说明。
 |
 | excludeColumnIndexes    | 空     | 需要排除对象中的 index 的数据                         
                                                                |
 | excludeColumnFieldNames | 空     | 需要排除对象中的字段的数据                              
                                                                |
 | includeColumnIndexes    | 空     | 只要导出对象中的 index 的数据                         
                                                                |
 | includeColumnFieldNames | 空     | 只要导出对象中的字段的数据                              
                                                                |
 | orderByIncludeColumn    | false | 在使用了参数 includeColumnFieldNames 或者 
includeColumnIndexes的时候,会根据传入集合的顺序排序                                     |
 
+#### 表头合并策略
+
+`headerMergeStrategy` 参数提供了对表头合并行为的精细控制:
+
+- **NONE**: 不进行任何自动合并。
+- **HORIZONTAL_ONLY**: 仅水平合并(同一行内的相同单元格)。
+- **VERTICAL_ONLY**: 仅垂直合并(同一列内的相同单元格)。
+- **FULL_RECTANGLE**: 仅合并完整的矩形区域(所有单元格名称相同)。
+- **AUTO**: 自动合并(默认行为,向后兼容)。
+
+**示例**:
+
+```java
+FastExcel.write(fileName)
+    .head(head)
+    .headerMergeStrategy(HeaderMergeStrategy.FULL_RECTANGLE)
+    .sheet()
+    .doWrite(data());
+```
+
+**注意**: 如果未设置 `headerMergeStrategy`,则根据 `automaticMergeHead` 决定行为:
+
+- `automaticMergeHead == true` → `HeaderMergeStrategy.AUTO`
+- `automaticMergeHead == false` → `HeaderMergeStrategy.NONE`
+
 ### WriteWorkbook 参数
 
 | 名称                      | 默认值                    | 描述                        
                      |
diff --git 
a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/head.md 
b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/head.md
index b9d6873c..32d89eda 100644
--- 
a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/head.md
+++ 
b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/head.md
@@ -78,3 +78,53 @@ public void dynamicHeadWrite() {
 ### 结果
 
 ![img](/img/docs/write/dynamicHeadWrite.png)
+
+---
+
+## 表头合并策略
+
+### 概述
+
+默认情况下,FastExcel 会自动合并名称相同的表头单元格。但是,您可以使用 `headerMergeStrategy` 参数来控制合并行为。
+
+### 合并策略
+
+- **NONE**: 不进行任何自动合并。
+- **HORIZONTAL_ONLY**: 仅水平合并(同一行内的相同单元格)。
+- **VERTICAL_ONLY**: 仅垂直合并(同一列内的相同单元格)。
+- **FULL_RECTANGLE**: 仅合并完整的矩形区域(所有单元格名称相同)。
+- **AUTO**: 自动合并(默认)。
+
+### 代码示例
+
+```java
+@Test
+public void dynamicHeadWriteWithStrategy() {
+    String fileName = "dynamicHeadWrite" + System.currentTimeMillis() + 
".xlsx";
+
+    List<List<String>> head = Arrays.asList(
+        Collections.singletonList("动态字符串标题"),
+        Collections.singletonList("动态数字标题"),
+        Collections.singletonList("动态日期标题"));
+
+    FastExcel.write(fileName)
+        .head(head)
+        .headerMergeStrategy(HeaderMergeStrategy.FULL_RECTANGLE)
+        .sheet()
+        .doWrite(data());
+}
+```
+
+### 常见使用场景
+
+**禁用合并**: 使用 `NONE` 完全禁用自动合并:
+
+```java
+FastExcel.write(fileName)
+    .head(head)
+    .headerMergeStrategy(HeaderMergeStrategy.NONE)
+    .sheet()
+    .doWrite(data());
+```
+
+**注意**: 旧的 `automaticMergeHead` 参数仍然支持以保持向后兼容。当未设置 `headerMergeStrategy` 时,行为由 
`automaticMergeHead` 决定。


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to