基于EasyExcel的单元格合并自定义算法处理

基于EasyExcel导出Excel后,通过对合并单元格的简单规则配置,实现如下图所示的单元格合并效果:

效果截图

原表格数据如下:

基于EasyExcel的单元格合并自定义算法处理_第1张图片

通过配置单元格合并规则后,生成的合并后的表格如下:

基于EasyExcel的单元格合并自定义算法处理_第2张图片

注:其中第三列,没有配置合并规则,数据保持不动

自定义合并规则类

如下代码类,是处理单元格合并的核心类,主要是按行计算相同数据进行单元格合并。

package com.shanhy.project.service.impl;

import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.shanhy.common.utils.JsonUtils;
import com.shanhy.project.vo.ExcelMergeStrategyModel;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.util.CellRangeAddress;

import java.util.ArrayList;
import java.util.List;

/**
 * Excel 行合并策略
 */
@Slf4j
public class CustomLoopMergeStrategy implements RowWriteHandler {

    //上一行
    private Row beforeRow = null;
    //合并规则(多个)
    private List<ExcelMergeStrategyModel> strategyList;
    //总行数(不含表头)
    private int dataRowTotalSize;
    //当前已经处理的行数(不含表头)
    private int dataRowCurrentSize = 0;

    private CustomLoopMergeStrategy() {
    }

    /**
     * 构造方法
     *
     * @param loopMergeStrategyJson 合并规则JSON,其中 columnName 为要被自动计算合并的列,relativeColumnNames 表示目标合并列需要参照的相关列(值全部相同则会触发合并目标目标列的单元格)
     *                              示例:
     *                              String  loopMergeStrategyJson =
     *                              [
     *                              {
     *                              "columnName": "A",
     *                              "relativeColumnNames": "A"
     *                              },
     *                              {
     *                              "columnName": "B",
     *                              "relativeColumnNames": "A,B"
     *                              },
     *                              {
     *                              "columnName": "C",
     *                              "relativeColumnNames": "A,B,C"
     *                              },
     *                              {
     *                              "columnName": "D",
     *                              "relativeColumnNames": "A,B,C,D"
     *                              }
     *                              ];
     * @param dataRowTotalSize      所有数据的行数
     */
    public CustomLoopMergeStrategy(String loopMergeStrategyJson, int dataRowTotalSize) {
        //记录excel行数
        this.dataRowTotalSize = dataRowTotalSize;
        //解析json 获取合并规则
        this.strategyList = JsonUtils.jsonToList(loopMergeStrategyJson, ExcelMergeStrategyModel.class);
    }

    @SneakyThrows
    @Override
    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
        //表头直接跳过
        if (isHead) {
            return;
        }
        //记录数据行数
        this.dataRowCurrentSize++;
        //记录beforeRow(第一行数据行 beforeRow = row)
        if (beforeRow == null) {
            beforeRow = row;
            return;
        }
        // 需要被合并的单元格,从第二行开始要被删除,否则会出现最终导出Excel后“选中合并的单元格下面的状态栏显示的数量不是1的情况“
        List<String> removeCellName = new ArrayList<>();
        // 循环所有规则并做相关处理
        for (ExcelMergeStrategyModel strategy : strategyList) {
            //执行 rowDataCompare() 根据当前规则进行判断 返回 true 和 false,返回true表示该列标记为可合并列
            if (this.rowDataCompare(row, beforeRow, strategy.getRelativeColumnNames().split(","))) {
                //mergeFlag:如果第一次出现重复数据,则进行合并标记(一般为即将合并列的第二行数据时标记)
                if (!strategy.isMergeFlag()) {
                    strategy.setMergeFlag(true);
                    strategy.setMergeStartRowIndex(row.getRowNum() - 1);
                } else {
                    removeCellName.add(strategy.getColumnName());
                }
                //判断是否最后一行 (最后一行直接执行合并)
                if (this.dataRowTotalSize == this.dataRowCurrentSize) {
                    this.addMergedRegion(strategy, row, row.getRowNum());
                    removeCellName.add(strategy.getColumnName());
                    // 处理完最后一行,直接结束
                    return;
                }
            } else {
                if (strategy.isMergeFlag()) {
                    //添加合并范围
                    this.addMergedRegion(strategy, row, beforeRow.getRowNum());
                    removeCellName.add(strategy.getColumnName());
                }
            }
        }
        // 清理当前行对应的列,避免单元格合并后计数不是1的问题
        if (!removeCellName.isEmpty()) {
            if (this.dataRowTotalSize == this.dataRowCurrentSize) {
                this.removeCell(row, removeCellName);
            } else {
                this.removeCell(beforeRow, removeCellName);
            }
        }
        beforeRow = row;
    }

    /**
     * 清理单元格的值
     *
     * @param row            行对象
     * @param columnNameList 单元格序号名称列表
     */
    private void removeCell(Row row, List<String> columnNameList) {
        for (String columnName : columnNameList) {
            row.removeCell(row.getCell(excelNum2Digit(columnName) - 1));
        }
    }

    /**
     * 添加合并范围
     *
     * @param strategy   规则对象
     * @param row        当前行
     * @param lastRowNum 合并单元格的最后一行序号
     */
    private void addMergedRegion(ExcelMergeStrategyModel strategy, Row row, int lastRowNum) {
        // 产生合并规则,从 mergeStartRowIndex 合并至 currentIndex - 1
        int currentCellIndex = excelNum2Digit(strategy.getColumnName()) - 1;
        CellRangeAddress cellRangeAddress = new CellRangeAddress(strategy.getMergeStartRowIndex(), lastRowNum, currentCellIndex, currentCellIndex);
        row.getSheet().addMergedRegion(cellRangeAddress);
        strategy.setMergeFlag(false);
        strategy.setMergeStartRowIndex(-1);
    }

    /**
     * 比较两行数据中的指定列的数据是否相同
     *
     * @param currentRow          当前行
     * @param beforeRow           上一行
     * @param relativeColumnNames 数值对比计算相对列
     * @return 所有相对列数值是否全部相同
     */
    private boolean rowDataCompare(Row currentRow, Row beforeRow, String[] relativeColumnNames) {
        if (beforeRow != null) {
            //取出规则进行判断
            for (String columnName : relativeColumnNames) {
                log.info("xxxxxxxxxxxx>>>>>>>>>>>{}", columnName);
                //当前列
                int cellIndex = excelNum2Digit(columnName) - 1;
                //判断当前行当前列的数据 和上一行规则内指定列的单元格数据 是否相同
                //例:第二行 A列的和B列的单元格数据要和 第一行的A列的和B列的单元格数据相同
                if (!currentRow.getCell(cellIndex).getStringCellValue().equals(beforeRow.getCell(cellIndex).getStringCellValue())) {
                    return false;
                }
            }
            //相对列的值全部相同返回true
            return true;
        } else {
            return false;
        }
    }


    /**
     * Excel 列号转数字 (A = 1 B = 2)
     *
     * @param excelNum Excel 列号
     * @return 数字
     */
    private int excelNum2Digit(String excelNum) {
        char[] chs = excelNum.toCharArray();
        int digit = 0;

        /*
         *   B*26^2 + C*26^1 + F*26^0
         * = ((0*26 + B)*26 + C)*26 + F
         */
        for (char ch : chs) {
            digit = digit * 26 + (ch - 'A' + 1);
        }
        return digit;
    }

    /**
     * 数字转 Excel 列号
     *
     * @param digit 数字
     * @return Excel 列号
     */
    private String digit2ExcelNum(int digit) {
        /*
         * 找到 digit 所处的维度 len, 它同时表示字母的位数
         * power 表示 26^n, 这里 n 分别等于 1, 2, 3
         * pre 表示 前 n 个维度的总和, 即 26^1 + 26^2 + 26^3
         */
        int len = 0, power = 1, pre = 0;
        for (; pre < digit; pre += power) {
            power *= 26;
            len++;
        }
        // 确定字母位数
        char[] excelNum = new char[len];
        /*
         * pre 包含 digit 所处的维度
         * pre - power 则是 digit 前面的维度总和
         * digit 先减去前面维度和
         */
        digit -= pre - power;
        /*
         * 比较难以理解的是这里为什么要自减 1
         * 其实是相对 (digit / power + 'A') 这句代码来的
         * 本应该是 (digit / power + 'A' - 1),
         * digit / power 的结果是完整的维度个数, 它加上 'A' - 1 后需要再加一
         * 当最后剩下的 6 个加上 'A' - 1 是应当的, 不需要做修改
         * 而当 (digit / power + 'A') 中没有减 1 后,
         * digit / power 的结果不需要再加一了
         * 相对于 digit / power 的结果, 最后剩下的 6 需要减 1
         */
        digit--;
        for (int i = 0; i < len; i++) {
            power /= 26;
            excelNum[i] = (char) (digit / power + 'A');
            digit %= power;
        }
        return String.valueOf(excelNum);
    }

    public void setDataRowTotalSize(int dataRowTotalSize) {
        this.dataRowTotalSize = dataRowTotalSize;
    }
    
}


@Data
class ExcelMergeStrategyModel {

    /**
     * 合并列,区分大小写只允许大写(例:A)
     * */
    private String columnName;

    /**
     * 参考列(例:A,B,C)
     * */
    private String relativeColumnNames;

    /**
     * 是否 合并标签
     * */
    private boolean mergeFlag = false;

    /**
     * 合并起始行
     * */
    private int mergeStartRowIndex = -1;

}


测试代码

@Slf4j
public class ExcelLoopMergeStrategyTest {

    public static void main(String[] args) {
        String s = "";
        String fileName = "e:\\MergeExcel-Demo1.xlsx";
        // String  loopMergeStrategyJson = 
		// [
		//   {
		//     "columnName": "A",
		//     "relativeColumnNames": "A"
		//   },
		//   {
		//     "columnName: "B",
		//     "relativeColumnNames: "A,B"
		//   }
		// ];

        String loopMergeStrategyJson = "[\n" + "  {\n" + "    \"columnName\": \"A\",\n" + "    \"relativeColumnNames\": \"A\"\n" + "  },\n" + "  {\n" + "    \"columnName\": \"B\",\n" + "    \"relativeColumnNames\": \"A,B\"\n" + "  }\n" + "]";
        CustomLoopMergeStrategy loopMergeStrategy = new CustomLoopMergeStrategy(loopMergeStrategyJson, 10);
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName, DemoMergeData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
    }

    private static List<DemoMergeData> data() {
        List<DemoMergeData> list = ListUtils.newArrayList();
        for (int i = 0; i < 10; i++) {
            DemoMergeData data = new DemoMergeData();
            if (i <= 7) {
                data.setStr1("字符串One");
            } else {
                data.setStr1("字符串One-" + i);
            }
            if (i <= 5) {
                data.setStr2("字符串Two");
            } else {
                data.setStr2("字符串Two-" + i);
            }
            if (i < 2) {
                data.setStr3("字符串Three");
            } else {
                data.setStr3("字符串Three-" + i);
            }
            list.add(data);
        }
        return list;
    }

}

@Data
class DemoMergeData {

    @ExcelProperty("标题1")
    private String str1;

    @ExcelProperty("标题2")
    private String str2;

    @ExcelProperty("标题3")
    private String str3;

}

(END)

你可能感兴趣的:(Java开发,excel)