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 92fa55dab..aae38a621 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.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 void initHead(ExcelWriteHeadProperty excelWriteHeadProperty) {
}
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 void initHead(ExcelWriteHeadProperty excelWriteHeadProperty) {
*
* @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 000000000..111bfb2e7
--- /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.
+ *
+ * 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}.
+ *
+ *
+ * @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 c30215ac9..e2ed55dde 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 @@
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 T automaticMergeHead(Boolean automaticMergeHead) {
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 5ce6f2114..c29cc4772 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 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 14031c46b..eae8c3fde 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.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 AbstractWriteHolder(WriteBasicParameter writeBasicParameter, AbstractWrit
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 boolean automaticMergeHead() {
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 a889f8cb7..b76ae88b1 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 1b0a621ce..4bf2e673f 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.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.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 ExcelWriteHeadProperty(
* Calculate all cells that need to be merged
*
* @return cells that need to be merged
+ * @deprecated Use {@link #headCellRangeList(HeaderMergeStrategy)} instead
*/
+ @Deprecated
public List headCellRangeList() {
- List cellRangeList = new ArrayList();
- Set alreadyRangeSet = new HashSet();
- List headList = new ArrayList(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 headCellRangeList(HeaderMergeStrategy mergeStrategy) {
+ if (mergeStrategy == null || mergeStrategy == HeaderMergeStrategy.NONE) {
+ return new ArrayList<>();
+ }
+
+ List cellRangeList = new ArrayList<>();
+ Set alreadyRangeSet = new HashSet<>();
+ List headList = new ArrayList<>(getHeadMap().values());
+
for (int i = 0; i < headList.size(); i++) {
Head head = headList.get(i);
List headNameList = head.getHeadNameList();
@@ -125,37 +144,144 @@ public List headCellRangeList() {
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 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 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 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 000000000..1dfc16cb5
--- /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> 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> 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> 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> 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> 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> createTestHead() {
+ List> 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> createTestData() {
+ List> 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/help/parameter.md b/website/docs/help/parameter.md
index e58a76eda..c1a22b7e6 100644
--- a/website/docs/help/parameter.md
+++ b/website/docs/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
@@ -164,13 +165,37 @@ All parameters inherit from `BasicParameter`.
| relativeHeadRowIndex | 0 | Number of rows to leave blank above Excel. |
| needHead | true | Whether to write the header to Excel. |
| useDefaultStyle | true | Whether to use default styles. |
-| automaticMergeHead | true | Automatically merge headers, matching the same fields above, below, left, and right in the header. |
+| automaticMergeHead | true | Automatically merge headers, matching the same fields above, below, left, and right in the header. **Note**: For more control, use `headerMergeStrategy` instead. |
+| 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/write/head.md b/website/docs/write/head.md
index 76c42061c..55e044413 100644
--- a/website/docs/write/head.md
+++ b/website/docs/write/head.md
@@ -75,3 +75,53 @@ public void dynamicHeadWrite() {
### Result

+
+---
+
+## 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> 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/help/parameter.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/parameter.md
index f44d2f294..17c449dae 100644
--- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/help/parameter.md
+++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/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
@@ -164,13 +165,37 @@ WriteWorkbook --|> WriteBasicParameter
| relativeHeadRowIndex | 0 | 写入到 excel 和上面空开几行 |
| needHead | true | 是否需要写入头到 excel |
| useDefaultStyle | true | 是否使用默认的样式 |
-| automaticMergeHead | true | 自动合并头,头中相同的字段上下左右都会去尝试匹配 |
+| automaticMergeHead | true | 自动合并头,头中相同的字段上下左右都会去尝试匹配。**注意**:如需更精细的控制,请使用 `headerMergeStrategy`。 |
+| 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/write/head.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/write/head.md
index 522f3a0e1..0ad2d85c9 100644
--- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/write/head.md
+++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/write/head.md
@@ -75,3 +75,53 @@ public void dynamicHeadWrite() {
### 结果

+
+---
+
+## 表头合并策略
+
+### 概述
+
+默认情况下,FastExcel 会自动合并名称相同的表头单元格。但是,您可以使用 `headerMergeStrategy` 参数来控制合并行为。
+
+### 合并策略
+
+- **NONE**: 不进行任何自动合并。
+- **HORIZONTAL_ONLY**: 仅水平合并(同一行内的相同单元格)。
+- **VERTICAL_ONLY**: 仅垂直合并(同一列内的相同单元格)。
+- **FULL_RECTANGLE**: 仅合并完整的矩形区域(所有单元格名称相同)。
+- **AUTO**: 自动合并(默认)。
+
+### 代码示例
+
+```java
+@Test
+public void dynamicHeadWriteWithStrategy() {
+ String fileName = "dynamicHeadWrite" + System.currentTimeMillis() + ".xlsx";
+
+ List> 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` 决定。