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

news2024/10/6 0:32:42

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

效果截图

原表格数据如下:

在这里插入图片描述

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

在这里插入图片描述

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

自定义合并规则类

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

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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/757557.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android Studio连接安卓手机

1. 创建项目 2. 下载Google USB Driver 点击右上角红框的【SDK Manager】->【SDK Tools】。 也可以在 【tools】->【SDK Manager】->【SDK Tools】下进入。 点击Google USB Driver&#xff0c;下载后点ok。 3. 环境变量 右键【我的电脑】->【高级系统设置】-&g…

基于微信小程序的高校新生自助报道系统设计与实现(Java+spring boot+MySQL+小程序)

获取源码或者论文请私信博主 演示视频&#xff1a; 基于微信小程序的高校新生自助报道系统设计与实现&#xff08;Javaspring bootMySQL微信小程序&#xff09; 使用技术&#xff1a; 前端&#xff1a;html css javascript jQuery ajax thymeleaf 微信小程序 后端&#xff1…

123、仿真-基于51单片机的电流控制仿真系统设计(Proteus仿真+程序+原理图+参考论文+配套资料等)

方案选择 单片机的选择 方案一&#xff1a;STM32系列单片机控制&#xff0c;该型号单片机为LQFP44封装&#xff0c;内部资源足够用于本次设计。STM32F103系列芯片最高工作频率可达72MHZ&#xff0c;在存储器的01等等待周期仿真时可达到1.25Mip/MHZ(Dhrystone2.1)。内部128k字节…

java报错- 类文件具有错误的版本 61.0, 应为 52.0 请删除该文件或确保该文件位于正确的类路径子目录中。

SpringBoot使用了3.0或者3.0以上&#xff0c;因为Spring官方发布从Spring6以及SprinBoot3.0开始最低支持JDK17&#xff0c;所以仅需将SpringBoot版本降低为3.0以下即可。

ES6类-继承-Symbol-模版字符串

目录 类 继承 ES5 如何继承 ES6继承 Symbol 用途 可以产生唯一的值&#xff0c;独一无二的值 解决命名冲突 getOwnPropertySymbols() 作为全局注册表 缓存 Symbol.for() 消除魔术字符串 模版字符串 类 在javascript语言中&#xff0c;生成实例对象使用构造函数&#xf…

数据库基本操作-----数据库用户管理和授权

目录 一、数据库用户管理 1&#xff0e;新建用户 2&#xff0e;查看用户信息 3&#xff0e;重命名用户 4&#xff0e;删除用户 ​编辑5&#xff0e;修改当前登录用户密码 6&#xff0e;修改其他用户密码 7&#xff0e;忘记 root 密码的解决办法 &#xff08;1&#xff09;修…

Redis数据类型 — List

List 列表是简单的字符串列表&#xff0c;按照插入顺序排序&#xff0c;可以从头部或尾部向 List 列表添加元素。 List内部实现 List 类型的底层数据结构是由双向链表或压缩列表实现的&#xff1a; 如果列表的元素个数小于 512 个&#xff08;默认值&#xff0c;可由 list-m…

详解Single-Shot Alignment Network (S2A-Net) 基于遥感图像的特征对齐旋转目标检测

引言 目标检测&#xff1a;把图像中的物体使用方框标记起来&#xff0c;不同类别物体应使用不同颜色 目标检测其实是寻找物体边界框(bounding box)回归问题(regression)和对物体分类问题(classification)的统一 遥感目标检测&#xff1a;普通的目标检测是日常生活中的横向的图…

2.4 线性表的插入删除

1. 链表的插入删除 1. 单链表插入删除 图1. 单链表插入结点 图2. 单链表删除结点 #include <iostream>typedef struct LNode {int data;struct LNode* next; }LNode;/// <summary> /// 判断链表是否非空 /// </summary> /// <param name"p">…

常见关于数组的函数的介绍

关于字符串函数的介绍 求字符串长度 strlen函数 用于计算字符串的长度的函数&#xff0c;需要使用的库函数是string.h 函数声明 size_t strlen(const char *str)函数模拟实现 #include<stdio.h> #include<assert.h> size_t my_strlen(const char* arr) {asse…

review回文子串

给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 class Solution {List<List<String>> lists new ArrayList<>(); // 用于存储所有可能…

阿里瓴羊One推出背后,零售企业迎数字化新解

配图来自Canva可画 近年来随着数字经济的高速发展&#xff0c;各式各样的SaaS应用服务更是层出不穷&#xff0c;但本质上SaaS大多局限于单一业务流层面&#xff0c;对用户核心关切的增长问题等则没有提供更好的解法。在SaaS赛道日渐拥挤、企业增长焦虑愈演愈烈之下&#xff0c…

Midjourney助力交互设计师设计网站主页

Midjourney的一大核心优势是提供创意设计&#xff0c;这个功能也可以用在网站主页设计上&#xff0c;使用Midjourney prompt 应尽量简单&#xff0c;只需要以"web design for..." or "modern web design for..."开头即可 比如设计一个通用SAAS服务的初创企…

单片机第一季:零基础5——LED点阵

1&#xff0c;第八章-LED点阵 如何驱动LED点阵&#xff1a; (1)单片机端口直接驱动。要驱动8*8的点阵需要2个IO端口&#xff08;16个IO口&#xff09;、要驱动16*16的点阵需要4个IO端口&#xff08;32个IO口&#xff09;。 (2)使用串转并移位锁存器驱动。要驱动16*16点阵只需要…

Linux 系统编程-开发环境(二)

目录 7 压缩包管理 7.1 tar 7.2 rar 7.3 zip 8 进程管理 8.1 who 8.2 ps 8.3 jobs 8.4 fg 8.5 bg 8.6 kill 8.7 env 8.8 top 9 用户管理 9.1 创建用户 9.2 设置用户组 9.3 设置密码 9.4 切换用户 9.5 root用户 9.6 删除用户 10 网络管理 10.1 i…

MySQL-分库分表详解(七)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xf…

【熬夜送书 | 第二期】清华社赞助 | 《前端系列丛书》

前端是什么? 前端&#xff08;Front-End&#xff09;&#xff0c;又称浏览器端、客户端等&#xff0c;是指 Web 应用程序中负责用户交互、界面展示和数据展示的部分。前端技术体系主要包括 HTML、CSS 和 JavaScript 等内容。 其中&#xff0c;HTML&#xff08;Hypertext Mar…

❀如何获得铁粉❀

文章目录 引言一、提供独特的价值1.1 分享专业知识和经验1.2 提供独特的产品或服务1.3 展示个人风格和个性 二、构建真实的关系2.1 回应评论和互动2.2 分享个人故事和经历2.3 建立信任和互信关系 三、提供独家福利3.1 提供折扣和促销3.2 推出限量版产品或服务3.3 独家活动和会员…

位运算常见算法题

文章目录 前言191. 位1的个数338. 比特位计数461. 汉明距离136. 只出现一次的数字260. 只出现一次的数字 III面试题 01.01. 判定字符是否唯一268. 丢失的数字371. 两整数之和137. 只出现一次的数字 II面试题 17.19. 消失的两个数字 前言 本篇文章会涉及多道位运算题目&#xf…

图像处理学习笔记(一)

目录 图像处理学习笔记&#xff08;一&#xff09;一、基础知识1、彩色图像&#xff08;1&#xff09;RGB&#xff08;2&#xff09;HSV&#xff08;3&#xff09;HSI&#xff08;4&#xff09;CMYK&#xff08;5&#xff09;YUV&#xff08;6&#xff09;YCbCr 2、灰度图像3、…