周一下午群里有运营反馈,导出的单据总金额和每项金额加起来对不上,差了一毛钱。目测是我的问题,赶紧回了句“稍等,正在看……”就去看代码了。当时产品和各业务线约定的是保留一位小数,没想到返回的数据中有些是两位小数。

第一反应是没有先保留一位小数再累加。比如三个 100.04,每项显示为 100.0,但先求和再保留一位小数,结果就是 300.1,正好差了 0.1。

再看实际数据,三项分别是 100、200.5、100.05,页面显示为 100.0、200.5、100.0,总金额则是 400.6。这里除了舍入顺序,还得先弄清楚:为什么 100.05 会显示成 100.0?

默认的舍入方式

FreeMarker 模板里统一这样保留一位小数:

${xx?string("0.0")}

100.05 变成 100.0,看起来像直接截取了小数;但总和 400.55 又显示为 400.6,看起来又做了四舍五入。

去看 FreeMarker 的 数字格式化说明,里面有这样一个例子:

${1.5?string("0")}
${2.5?string("0")}

结果都是 2:

2
2

它默认使用的是 HALF_EVEN,也就是通常说的“四舍六入五成双”。先取最接近的值;刚好在两个值正中间时,选择末位为偶数的那个。

保留一位小数时,100.05 正好处在 100.0 和 100.1 中间,取末位为偶数的 100.0;400.55 则处在 400.5 和 400.6 中间,取 400.6。这样就解释得通了。

这里“遇到 5 看奇偶”有个前提:5 后面没有其他非零数字。比如 100.051 保留一位小数时,更接近 100.1,就应该进位。

明确指定舍入规则

Java 的 BigDecimal 也支持这两种舍入方式:

BigDecimal amount = new BigDecimal("100.05");
System.out.println(amount.setScale(1, RoundingMode.HALF_EVEN));
System.out.println(amount.setScale(1, RoundingMode.HALF_UP));

输出:

100.0
100.1

如果业务约定采用通常的四舍五入,就可以在计算时明确使用 RoundingMode.HALF_UP,再把处理后的金额交给模板显示。这里用字符串构造 BigDecimal,避免先经过 double 带入二进制浮点误差。

不过,指定 HALF_UP 也不能自动解决所有总金额差异。前面三个 100.04 的例子依然存在,所以还要约定清楚:总金额是把每项保留一位后相加,还是先按原始金额求和,再保留一位。计算和显示需要采用同一套口径。

Locale 也有类似的问题

默认值带来的差异,在日期格式化里也会遇到。例如没有指定 Locale:

SimpleDateFormat df = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE");

同一个时间,在中文和英文环境中,星期部分可能分别显示为:

2018年04月15日 14:21:08 星期日
2018年04月15日 14:21:08 Sun

如果页面需要固定显示中文,就把 Locale 写清楚:

SimpleDateFormat df = new SimpleDateFormat(
        "yyyy年MM月dd日 HH:mm:ss EEE", Locale.CHINA);

这些默认行为平时不太容易注意到,等输出结果和预期不一样时,再去看配置和文档,往往就能找到原因。

最后修改:2018 年 04 月 16 日
如果觉得我的文章对你有用,请随意赞赏