计算表达式round四舍五入不正确的原因与解法原创
金蝶云社区-云社区用户75580314
云社区用户75580314
16人赞赏了该文章 5,657次浏览 未经作者许可,禁止转载编辑于2021年01月21日 17:08:53
  • 问题

计算定义公式的值并填写到指定列round( 3025 * 0.033, 2),算出来的结果是99.82,应该是99.83才对

 

  • 重现问题

1.       找个单据,加2个小数字段,2个文本字段。

2.       给 文本1 字段配个值更新事件,方便触发。计算两个小数字段相乘并四舍五入保留2位小数,结果填入 文本a 字段。如下图:

10.png

3.       进入运行时,新建一张单据,给两个小数字段分别填入3025、0.033,然后随便改一下 文本1 字段的值触发事件,计算结果为 99.83。

20.png

计算一下看是否正确:

40.png

Python2的round函数是四舍五入,所以应该是99.83,正确。

4.       然后保存单据,关闭再打开,修改一下 文本1 字段触发事件,计算结果为99.82。这就不对了。

30.png

5.       再试试,修改3025为3000,修改一下 文本1 字段触发事件。然后再改回3025,然后再修改一下 文本1 字段触发事件,计算结果为99.83。看一下http数据:

50.png

可见传去服务端的是3025.0。

6.       关掉再打开单据,看看打开单据时服务端传来的值是多少:

60.png

70.png

可见服务端的值是3025.0000000000。这是因为数据库字段是decimal(23,10),所以数据库中存的就是这个。

80.png

90.png

7.       那么,问题就变成了,为什么3025.0000000000算出来就是99.82。


  • 排查

1.       BOS处理python表达式用的是第三方组件IronPython (2.6.10920.0),写个测试程序看看,代码如下:

100.png

简单地调IronPython,计算round( a * b, 2)的值。

运行结果:

110.png

可见,当3025后面有10个小数0时,计算结果99.82,不正确。当后面有8个小数0时,或没有0时,计算结果99.83,正确。多试几种情况发现,小数的位数为9、10时,结果为99.82,不正确。小数的位数为0-8时,结果为99.83,正确。而且结果类型是double。

2.       再把代码中的表达式改为a*b,看看运行结果:

140.png

都是99.825后面若干个0,结果类型是decimal。

3.       再改一下,直接计算round(a,2):

150.png

160.png

可见,99.82500000000000000000 四舍五入出来就是99.82,不正确。当少3个小数0后,99.82500000000000000四舍五入出来就是99.83,正确。

4.       考虑到round函数结果变成了double,可以大胆推测一下,IronPython计算round时,将传入的decimal转为double,再调用C#的Math.Round,最终返回了double类型的值。

写个代码验证一下,看看decimal的99.82500000000000000000转double后四舍五入结果是多少:

170.png

断点调试看一看:

180.png

可见decimal的99.82500000000000000000转double后变成了99.824999999999989,那么四舍五入自然就变成了99.82。

190.png

少3个0的99.82500000000000000计算是正确的。

5.       那么问题就找到了。decimal的两个10位小数相乘,结果的小数变成20位。IronPython计算表达式的round函数,会先将decimal转为double,由于double的精度问题,此时可能出现误差,最终造成结果不准确。


  • 变通解决方案

由于python对decimal的支持需要引入decimal模块,在单行的表达式中显然不行。double类型又确实有精度问题。所以可以考虑用变通方案:


1.  写个插件处理,直接对decimal数值使用Math.Round,注意要传MidpointRounding.AwayFromZero才是四舍五入,否则是四舍六入五成双。这种方法最完美,就是稍显麻烦。


2.  既然小数位数太多可能有精度问题,那就根据业务需求减少小数位数,毕竟需要支持10位小数的业务也不多。在设计单据时,小数字段精度调低一些,不要用默认的10位。注意在保存单据之后再修改精度,是不会修改数据库字段的精度的。所以这只适合新开发的单据,或者自己去改数据库字段精度。


3.  仍然考虑减少小数位数。比如例子中的3025,业务上最多只会有2位小数,例子中的0.033最多只会有4位小数,那么表达式这样写:

F_MOB_Text =round( round( F_MOB_Decimal, 2) * round( F_Jac_Decimal, 4) ,2)

200.png

这样先减少无效小数位数再计算,精度问题会少一些。

赞 16