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 ![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> 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() { ### 结果 ![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> 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` 决定。