JQuick-Excel 实战:FORMAT 管理日期与数字的 Excel 显示
tags: Java, Excel, JQuick-Excel, 报表导出, FORMAT, 日期格式, 数字格式
简介
日期显示成连续数字、金额缺少分组分隔符、比例被看成普通小数,常常不是源数据本身错误,而是工作簿没有得到合适的显示格式。JQuick-Excel 的导出 DSL 提供 FORMAT,README-CN 将其定义为 Excel 显示格式,并给出 FORMAT={"amount":"currency"}、FORMAT={"enrollmentDate":"yyyy-MM-dd"} 等写法。本文以这些已确认事实为界,说明如何把字段级显示规则写入 EXPORT WITH,并区分 FORMAT、TRANSFORM、STYLE 各自承担的责任。
本文使用 Maven io.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 坐标。
xml
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>
导出规则由 XML 中的 <excel> 承载,EXPORT WITH 下可同时声明工作表、表头、映射和显示格式。下例遵循 README-CN 的字段到表头映射方向:左侧是源字段名,右侧是输出表头。FORMAT 的键仍然是左侧字段名。
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE excels PUBLIC "-//PAOHAIJIAO//DTD API EXCEL 1.0//EN"
"classpath:paohaijiao/dtd/Jquick-excel.dtd">
<excels namespace="com.example.excel.OrderExcelService">
<excel name="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 后创建接口代理。
java
import com.github.paohaijiao.convert.JObjectConverter;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.JQuickFactory;
import com.github.paohaijiao.xml.JQuickXmlFactory;
import com.github.paohaijiao.xml.parse.JQuickParseHandler;
import com.github.paohaijiao.xml.parse.excel.JQuickExcelExportXmlParseFactory;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
List<Map<String, Object>> data = loadOrders();
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(data));
try (OutputStream output = new FileOutputStream("orders.xlsx")) {
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(rows, output);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
OrderExcelService service = factory.createApi(OrderExcelService.class);
service.exportOrders("field", "value");
}
字段值应按业务语义准备。日期字段应保留为应用中可正确导出的日期值,金额、数量和比例也应保留可计算的数值含义,不要为了获得逗号或百分号而在数据准备阶段先拼接展示文本。FORMAT 的作用正是在输出工作簿时处理显示层。下例仅表达字段数据与 XML 字段名的对应关系;没有把显示内容预先写进 amount 或 completionRate。
java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
List<Map<String, Object>> data = new ArrayList<>();
Map<String, Object> order = new LinkedHashMap<>();
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 显示控制。因此若文件只要求"日期列显示为年-月-日",可集中使用字段级格式规则。
xml
<excel name="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 负责日期显示,二者不承担对方职责。
xml
<excel name="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,而不要求把日期先转换成字符串。
java
import com.github.paohaijiao.context.JContext;
Map<String, Object> gender = new LinkedHashMap<>();
gender.put("1", "Male");
gender.put("0", "Female");
JContext context = new JContext();
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 能被读取便认定各客户端表现一致,也不要在字段和坐标两处重复声明同一显示语义,避免后续无法判断配置来源。