Enjoy模板中小数运算结果位数?

2020-3-15 反馈原文

刚在Enjoy模板中需要使用除法计算,数据源Record中的2个都是小数(2位),html如下:

  <tr>
    <th class="center aligned">合计</th>
    <th class="right aligned">#(sum.budget)</th>
    <th class="right aligned">#(sum.expense)</th>
    <th>#(sum.expense / sum.budget)</th>
    <th>#(10396.08 / 954095.26)</th>
  </tr>

如上,第4列中的sum.expense的值和sum.budget的值分别是10396.08 、954095.26,和第5列相等,但是渲染结果是:

合计
954095.26
10396.08
0.010.010896270462553183

结果分别是0.01和0.010896270462553183。

这里#(sum.expense / sum.budget)总是得到2位小数,测试多次也是如此,但是直接用数字就可以得到足够小数位,想问一下Enjoy中的小数位数是如何默认的。

实际上想得到最少4位结果以便显示百分比:
#(sum.expense / sum.budget, "0.00%")。

---------------------------------------------------------------------

2020-3-25 测试结果反馈

根据James的提示,对Arith进行了调试。
不出所料,enjoy中对位数最主要影响的代码出现在div方法里。

private Number div(int maxType, Number left, Number right) {
   switch (maxType) {
   case INT:
      return Integer.valueOf(left.intValue() / right.intValue());
   case LONG:
      return Long.valueOf(left.longValue() / right.longValue());
   case FLOAT:
      return Float.valueOf(left.floatValue() / right.floatValue());
   case DOUBLE:
      return Double.valueOf(left.doubleValue() / right.doubleValue());
   case BIGDECIMAL:
      BigDecimal[] bd = toBigDecimals(left, right);
      // return (bd[0]).divide(bd[1]);
      return (bd[0]).divide(bd[1], RoundingMode.HALF_EVEN);  // 银行家舍入法
   }
   throw new TemplateException("Unsupported data type", location);
}

从代码可以看出,最核心的结果是出现在第14行:

return (bd[0]).divide(bd[1], RoundingMode.HALF_EVEN);

这里BigDecimal的divide方法中,scale默认采用了bd[0]的scale:

Returns a BigDecimal whose value is (this / divisor), and whose scale is this.scale(). If rounding must be performed to generate a result with the given scale, the specified rounding mode is applied.

因此,计算结果保留2位小数是可以预见的。
而且,结果的位数与格式化指令#number()是没关系的,因为#number()只是对结果进行了格式化,而无法影响devide的计算精度;结果只有2位小数的情况下,#number()只能格式化成1.00%。

第二,可以验证,#number(bd, "#.##%")与#number(bd, "0.00%")并非影响因素。
#number(0.012, "#.##%")会被格式化为1.2%;
#number(0.012, "0.00%")会被格式化为1.20%而已(用0补足位数)。

如下是调试截图:

image.png

第三,在enjoy中对所有bigDecimal采用half_even的舍入行为,而不是四舍五入,不知是否妥当,虽然该舍入方法在统计上更加精确(流行于美国)。
是否有更加灵活的方式可以指定精度或者舍入方法。。。



评论区

JFinal

2020-03-15 20:34

enjoy 的数值运算完全依赖于 java 内部的规则,只是输出的时候需要指定格式

需要用到 #number 指令来输出,输出的格式仍然使用 java 原有规则,不增加任何学习成本,注意看 #number 指令:
https://jfinal.com/doc/6-4

由于 #() 输出指令不指定格式,所以就跟 System.out.println(...) 输出的行为一样

himans

2020-03-24 11:47

#(sum.expense) = 10396.08; #(sum.budget)=954095.26;
然而:#(sum.expense / sum.budget) = 0.01(导致百分比是1.00%,百分比没有小数位)。
而#(10396.08 / 954095.26)=0.010896270462553183(百分比是1.09%,百分比有小数位,这是想要的结果)。

因为试了多次无法在Enjoy中控制小数位数,就只好在后台直接用BigDecimal计算好4位小数再传到Enjoy模板。@JFinal

JFinal

2020-03-24 11:53

@himans 用 #number 指令,不要用输出指令

himans

2020-03-24 17:38

@JFinal 其实一开始的表达式就是number表达式:#(sum.expense / sum.budget, "0.00%"),得出1.00%,所以才测试直接用 #() 输出的。

JFinal

2020-03-24 18:39

@himans
需要调试 enjoy Arith.java 的计算细节,你那正好有这个测试用例

希望你能就你当前碰到的这些数值在 Arith.java 内调试,看每一步计算的结果,到底哪一步与预期不符

希望你继续反馈这个问题

JFinal

2020-03-24 18:40

具体的类在 com.jfinal.template.expr.ast.Arith.java

JFinal

2020-03-24 18:45

@himans 我发现你的 #number 指令传递的 format 表达式不对,改成下面用法试一下:
#number(sum.expense / sum.budget, "#.##%")

不是 "0.00%", 而是 "#.##%"

注意要用 #number 指令来测试

JFinal

2020-03-24 19:01

这里有几点说明:
1:#(sum.expense / sum.budget) 这种用法,sum.expense 与 sum.budget 变量的值取决于其本身的类型,这个一定要通过调试搞清楚,到底是 float 还是 double 类型

2:#(10396.08 / 954095.26) 这种用法,其中的 10396.08 与 954095.26 常量会被 enjoy 转换成 Double 类型

所以,确定 sum.expense 与 sum.budget 变量的类型是关键,如果这两个变量的类型都是 Double 的话,那输出结果应该是完全一样的

通过上面的推断, sum.expense 与 sum.budget 变量的类型应该不是 Double

himans

2020-03-25 09:57

@JFinal 非常感谢详细回复。测试数据类型这正是我想要的。会继续汇报结果。

himans

2020-03-25 11:05

@JFinal 的确,Arith中是用BigDecimal来计算的,与之前你我猜想的一致。
因为回复里不支持超文本,调试结果已经修改在反馈正文里面了,请看正文《2020-3-25 测试结果反馈》

JFinal

2020-03-25 12:19

@himans 我估计你用的是 oracle,所以从数据库查出来直接就是 BigDecimal 类型

当前的 enjoy 不支持定制 Arith.java 的行为,当前可能最好的办法是你在后台通过 java 代码先计算出这个值,然后 html 中输出一下:
BigDecimal budget = sum.getBigDecimal("budget");
BigDecimal expense = sum.getBigDecimal("expense");
BigDecimal percent = budget.divide(expense, 这里用上自己实际需要的参数);

// 这里将 percent 再放回 record 对象中,便于在 html 中输出
sum.set("percent", percent);

html 中的输出方式:
#number(sum.percent, "#.##%")

注意上面第三行代码中的 divide 方法,可以指定 scale 与舍入方式,例如:
budget.divide(expense, 4, RoundingMode.HALF_UP);

以上 divide 中的 4 可以保留四位小数, RoundingMode.HALF_UP 即为四舍五入

himans

2020-03-25 13:46

@JFinal 谢谢!只要搞清楚核心问题就好。不寻求修改Enjoy规则。反馈只是希望对你以后升级有用。
用的是MySQL,DB中数据类型是decimal(9,2),SQL语句是用的聚合函数sum()得到Record。
15日反馈之后就采用了先在后台计算出4位小数再传到前台的方法,跟你建议的一样。

JFinal

2020-03-25 16:49

@himans 我现在想对这个地方进行改进,现在有两个方案,看你那里希望选择哪一个?

方案一:
提供一个 setBigDecimalDivideMinScale(int) 方法用来配置 BigDecimal 除法的最小 scale,默认值为 5
然后代码这样用:
int scale = Math.max(bigDecimalDivideMinScale, (bd[0]).scale());
(bd[0]).divide(bd[1], scale, RoundingMode.HALF_EVEN);

也就是说 scale 取值为被除数的 scale 与配置值取最大值

方案二:
取被除数与除数中最大的 scale 值:
int scale = Math.max((bd[0]).scale(), (bd[1]).scale());
(bd[0]).divide(bd[1], scale, RoundingMode.HALF_EVEN);

结合你的应用场景,你觉得哪种方案最好?

在你的当前场景之外,你觉得哪个方案更好?

JFinal

2020-03-25 16:51

方案二能否满足你当前场景的需求?

himans

2020-03-26 09:29

@JFinal 非常感谢!
这两个方案,从实用性上来说,可能第一个好一点。因为一般来说,应用中Decimal类型多用于金额和数量,而金额的情况下,大多是是2位数,如果要计算价格(或比率),那么可能还是会产生位数不够的问题。如果采用这个方案,那么最好RoundingMode也可以指定,因为国内主要使用“四舍五入”HALF_UP。
从灵活性上来说,第二个好一些,但是必须先给一个除数或被除数在后台指定scale(需要较多位数的情况下,这样在select一个集合的情况下可能比较麻烦),这个虽然不够简洁,但也似乎没有更简洁的办法。或者取left和right的scale的和?
当然二个方案都支持是更好的。
另外作为Enjoy模板,有“安全取值”的思想,蛮好的,是否有必要在除法里面也反映进去,比如,当除数为0时肯定异常,安全取值时,除数为0则渲染成空字符或0,避免每次都要判断(当然要判断也是很容易的事情:#(b==0?0:a/b)。

JFinal

2020-03-26 12:49

@himans RoundingMode 这配置早已决定添加,所以上面的方案中并未谈到,并且已经决定采用四舍五入,毕竟很多人不太了解 "银行家舍入法"

scale 决定采用第一个方案,默认为 5 这个是否能满足你的绝大多数需求?

默认为 5 是这么考虑的:平常用到小数点两位,有时因为百分比需求要用到 4 位,再额外加 1 位增加精度

himans

2020-03-26 16:50

@JFinal 如果是方案1,那么5应该是够了,毕竟还可以指定。

JFinal

2020-03-26 23:05

@himans 已改进:
1:默认 scale 为 5
2:默认采用四舍五入
https://gitee.com/jfinal/jfinal/commit/cf791ec9a2f7ef14aa70b728e7ca58122a6184ec

试用后请反馈给我,我在本地测试非常完美

himans

2020-03-27 10:44

@JFinal 了解,谢谢~