ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JQuick-Excel 实战:FORMAT 管理日期与数字的 Excel 显示

JQuick-Excel 实战:FORMAT 管理日期与数字的 Excel 显示

JQuick-Excel 实战:FORMAT 管理日期与数字的 Excel 显示

tags: Java, Excel, JQuick-Excel, 报表导出, FORMAT, 日期格式, 数字格式

更多示例: JQuick-Excel 官网示例 https://www.jquick.org/jquick-excel/#demo-formulas0005

简介

日期显示成连续数字、金额缺少分组分隔符、比例被看成普通小数,常常不是源数据本身错误,而是工作簿没有得到合适的显示格式。JQuick-Excel 的导出 DSL 提供FORMAT,README-CN 将其定义为 Excel 显示格式,并给出FORMAT={"amount":"currency"}、FORMAT={"enrollmentDate":"yyyy-MM-dd"}等写法。本文以这些已确认事实为界,说明如何把字段级显示规则写入EXPORT WITH,并区分FORMAT、TRANSFORM、STYLE各自承担的责任。

本文使用 Mavenio.github.paohaijiao:jquick-excel:3.6.0与 Java 8+。事实仅来自 README-CN 和src/test/resources/jquick-excel.xml:测试 XML 中出现了dateFormat(${enrollmentDate},'yyyy-MM-dd')的转换,README-CN 明确说明FORMAT独立负责转换后的 Excel 单元格显示。因此不能把FORMAT误写成未知的类型转换、模板替换或单元格公式能力。

前言

Excel 的值与显示不是同一件事。业务数据可以是日期、数值或其他类型,用户在网格中看到的内容则由单元格显示规则决定。一个日期可以按年、月、日显示,也可以携带时间;一个数值可以显示小数位、分组符号或百分号。导出报表如果只关注“看起来像”,很容易在 Java 侧提前把值拼成文本。这样得到的文件虽然视觉上可能暂时正确,却可能损失后续筛选、排序、统计和公式计算需要的数值或日期语义。

FORMAT的价值就在于将展示要求声明在导出规则中。MAPPING负责源字段到表头的映射,FORMAT负责字段在 Excel 中的显示,TRANSFORM负责值表达式转换,STYLE负责样式目标。把这些职责拆开,阅读 XML 时能快速定位问题:列标题不对先看MAPPING,日期或数值外观不对先看FORMAT,字段值被翻译或改写先看TRANSFORM,字体与对齐等视觉效果再看STYLE。

README-CN 对导出的描述包括工作表、表头、映射、格式、转换、公式、样式、合并、图表和页脚。这里的“格式”不是泛指所有视觉属性,而是表格中单独的FORMAT配置项。它面向字段名,而不是行号、列字母或表头文字。因而调整字段名时,应同步检查MAPPING、FORMAT和TRANSFORM;只改表头文案时,通常不应把字段级显示规则一起改成表头名称。

环境与依赖

README-CN 明确 JQuick-Excel 支持xls和xlsx,要求 Java 8+。本文使用其公开的 Maven 坐标。

<dependency><groupId>io.github.paohaijiao</groupId><artifactId>jquick-excel</artifactId><version>3.6.0</version></dependency>

导出规则由 XML 中的<excel>承载,EXPORT WITH下可同时声明工作表、表头、映射和显示格式。下例遵循 README-CN 的字段到表头映射方向:左侧是源字段名,右侧是输出表头。FORMAT的键仍然是左侧字段名。

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEexcelsPUBLIC"-//PAOHAIJIAO//DTD API EXCEL 1.0//EN""classpath:paohaijiao/dtd/Jquick-excel.dtd"><excelsnamespace="com.example.excel.OrderExcelService"><excelname="exportOrders"returnClass="void"><![CDATA[ EXPORT WITH SHEET="订单报表", HEADER=true, MAPPING={ "orderNo":"订单号", "orderDate":"下单日期", "amount":"订单金额", "quantity":"数量", "completionRate":"完成率" }, FORMAT={ "orderDate":"yyyy-MM-dd", "amount":"currency" } ]]></excel></excels>

项目测试资源的导出 XML 也说明了另一项关键边界:enrollmentDate可在TRANSFORM中使用dateFormat(${enrollmentDate},'yyyy-MM-dd'),同时 README-CN 又给出FORMAT={"enrollmentDate":"yyyy-MM-dd"}。两种配置都与日期有关,但目标不相同。本文中只要需求是 Excel 的显示形式,就优先用FORMAT;只有确实需要求值器把字段值转换为另一个值时,才讨论TRANSFORM。

代码示例

Java 侧根据 README-CN 的 XML 导出链路准备行数据和输出流。数据通过JObjectConverter与JQuickRow.toRows转为行,再将行和输出流交给JQuickExcelExportXmlParseFactory。JQuickXmlFactory读取 XML 后创建接口代理。

importcom.github.paohaijiao.convert.JObjectConverter;importcom.github.paohaijiao.statement.JQuickRow;importcom.github.paohaijiao.xml.JQuickFactory;importcom.github.paohaijiao.xml.JQuickXmlFactory;importcom.github.paohaijiao.xml.parse.JQuickParseHandler;importcom.github.paohaijiao.xml.parse.excel.JQuickExcelExportXmlParseFactory;importjava.io.FileOutputStream;importjava.io.OutputStream;importjava.util.List;importjava.util.Map;List<Map<String,Object>>data=loadOrders();List<JQuickRow>rows=JQuickRow.toRows(JObjectConverter.convert(data));try(OutputStreamoutput=newFileOutputStream("orders.xlsx")){JQuickParseHandlerparser=newJQuickExcelExportXmlParseFactory(rows,output);JQuickFactoryfactory=newJQuickXmlFactory(parser,"jquick-excel.xml");OrderExcelServiceservice=factory.createApi(OrderExcelService.class);service.exportOrders("field","value");}

字段值应按业务语义准备。日期字段应保留为应用中可正确导出的日期值,金额、数量和比例也应保留可计算的数值含义,不要为了获得逗号或百分号而在数据准备阶段先拼接展示文本。FORMAT的作用正是在输出工作簿时处理显示层。下例仅表达字段数据与 XML 字段名的对应关系;没有把显示内容预先写进amount或completionRate。

importjava.util.ArrayList;importjava.util.LinkedHashMap;importjava.util.List;importjava.util.Map;List<Map<String,Object>>data=newArrayList<>();Map<String,Object>order=newLinkedHashMap<>();order.put("orderNo","SO-1001");order.put("orderDate",orderDateValue());order.put("amount",amountValue());order.put("quantity",12);order.put("completionRate",rateValue());data.add(order);

日期字段最直接的已确认示例是yyyy-MM-dd。README-CN 同时在TRANSFORM部分给出dateFormat(${enrollmentDate},'yyyy-MM-dd'),并将FORMAT描述为最终 Excel 显示控制。因此若文件只要求“日期列显示为年-月-日”,可集中使用字段级格式规则。

<excelname="exportStudents"returnClass="void"><![CDATA[ EXPORT WITH SHEET="学生表", HEADER=true, MAPPING={ "id":"主键", "name":"姓名", "enrollmentDate":"入学时间" }, FORMAT={ "enrollmentDate":"yyyy-MM-dd" } ]]></excel>

当业务确实需要转换值时,再加TRANSFORM。README-CN 已确认内置dateFormat、toUpper和trans,并说明${field}读取当前行字段、${key}可读取JContext值。下面的规则展示三种配置并列存在的清晰方式:TRANSFORM改变姓名和性别字段的输出值,FORMAT负责日期显示,二者不承担对方职责。

<excelname="exportStudents"returnClass="void"><![CDATA[ EXPORT WITH SHEET="学生表", HEADER=true, MAPPING={ "name":"姓名", "gender":"性别", "enrollmentDate":"入学时间" }, FORMAT={"enrollmentDate":"yyyy-MM-dd"}, TRANSFORM={ "name":toUpper(${name}), "gender":trans(${dict},${gender}) } ]]></excel>

Java 侧若使用字典转换,可按 README-CN 将字典放入JContext。日期显示格式仍留在 XML 的FORMAT,而不要求把日期先转换成字符串。

importcom.github.paohaijiao.context.JContext;Map<String,Object>gender=newLinkedHashMap<>();gender.put("1","Male");gender.put("0","Female");JContextcontext=newJContext();context.put("dict",gender);

原理说明

FORMAT是字段级的 Excel 显示规则。README-CN 的导出参数表把它列为“定义 Excel 显示格式”,并提供FORMAT={"amount":"currency"}与FORMAT={"date":"yyyy-MM-dd"}的用法。它与MAPPING的关系是:同一个字段先通过映射决定写到哪个表头列,再由格式决定该字段单元格的显示方式。它不是坐标定位 DSL,所以不应将A2、ROW 1等样式目标写进字段格式配置。

日期显示应与值转换区分。测试 XML 里的dateFormat(${enrollmentDate},'yyyy-MM-dd')属于TRANSFORM表达式,README-CN 也明确写出TRANSFORM为每一行计算表达式。FORMAT则“独立负责转换后的 Excel 单元格显示格式”。如果目标是保留字段的日期语义,同时让用户看到特定形式,字段格式比把日期预先改为文本更符合这一定义。

数值也是同一原则。金额、数量和比例的显示要求不等于字段值的计算要求。README-CN 以FORMAT={"amount":"currency"}展示金额字段的格式配置。实际采用何种数值展示口径应由业务决定,并应先统一输入数据含义,再配置对应的 Excel 显示规则。不要依赖显示规则修复已经以错误量纲传入的数值。

STYLE与FORMAT的分工也需要明确。README-CN 的样式参数包括字体、字号、粗体、斜体、颜色、对齐、边框和填充等;同时列出了dataFormatString。但 README-CN 也专门为FORMAT提供字段级日期和数字显示入口。对于常规字段展示,使用FORMAT可以直接表明意图;对于明确坐标的视觉布局,再使用STYLE。同一字段不要在多处重复定义相同的显示语义,否则维护者无法判断哪份配置是权威来源。

TRANSFORM的值转换可以使用当前字段和上下文。README-CN 的链路是:当前行字段或JContext值,经过${field}解析,进入TRANSFORM表达式和函数查找,得到转换结果,最后由FORMAT控制 Excel 显示。理解这个顺序有助于排障:字典文本不正确时检查上下文和trans;日期视觉效果不正确时检查FORMAT;字段根本没有出现在目标列时检查MAPPING。

注意事项

第一,FORMAT用于显示,不是数据清洗机制。格式字符串不能将任意文本自动变为可计算金额,也不能替代业务层对空值、非法值和默认值的处理。源数据应先符合字段语义。

第二,格式键必须使用导出源字段名。README-CN 的MAPPING={"id":"ID"}表示左边是字段、右边是表头;FORMAT={"amount":"currency"}同样以字段为键。把表头文字或 Excel 列字母写成格式键,会破坏这一契约。

第三,日期格式要与模板示例和业务口径保持一致。yyyy-MM-dd是 README-CN 已确认的形式。涉及日期时间、不同区域显示或复杂格式时,应以真实工作簿在目标 Excel 客户端验证,而不要仅以 XML 可解析作为验收结论。

第四,避免同时把日期转换为文本又再对其声明日期显示格式。若必须使用dateFormat做值转换,应清楚说明输出预期;若只需要 Excel 外观,优先使用FORMAT。同一字段只保留一个主要展示策略,后续维护才可追踪。

第五,比例、金额和数量应使用真实样本验收。至少覆盖零值、整数、带小数的值、大值、空值以及业务允许的边界值。验收不应只看截图,还要检查筛选、排序和公式计算是否仍符合报表目的。

第六,FORMAT与主题、页脚、公式、样式可以同处一个EXPORT WITH规则,但它们是不同能力。修改格式时不要顺手改变TRANSFORM或STYLE的语义;保持改动范围小,有助于定位导出回归。

总结

日期和数字列的首要判断是“值是否仍保留业务语义”,其次才是“用户看见什么”。FORMAT是导出字段级显示规则,适合用已确认的yyyy-MM-dd或currency等形式表达外观;不要为了分组符、日期写法或比例观感,预先在 Java 中把可计算的值拼成文本。展示问题与源数据量纲错误是两类问题,格式配置不能替后者兜底。

检查时应把键名和数据链路一起看。FORMAT的键应是导出源字段,而非表头文本或 Excel 列号;字段重命名时,需同步复核MAPPING、TRANSFORM与格式配置。日期仅需调整单元格外观时交给FORMAT,确需计算出另一个字段值时才使用TRANSFORM,字体、颜色和坐标布局则留给STYLE。

真实文件是这类规则的最终验证对象。金额、数量和比例至少应覆盖零、整数、小数、大值、空值及业务边界,并检查筛选、排序和公式计算仍符合报表目的;日期还应在目标 Excel 客户端检查显示效果。不要根据 XML 能被读取便认定各客户端表现一致,也不要在字段和坐标两处重复声明同一显示语义,避免后续无法判断配置来源。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进