本文描述了在使用IronPython进行小数运算时,遇到的round函数计算结果不准确的问题。具体为当两个高精度的decimal小数相乘后再进行round操作时,由于先将decimal转换为double导致的精度损失,造成最终结果错误。文中通过实验和代码分析,确认了问题的根源,并提供了三种可能的解决方案:使用Math.Round处理decimal值、减少小数字段的精度、或在计算前对小数进行预处理以减少精度损失。
问题
计算定义公式的值并填写到指定列round( 3025 * 0.033, 2),算出来的结果是99.82,应该是99.83才对
重现问题
1. 找个单据,加2个小数字段,2个文本字段。
2. 给 文本1 字段配个值更新事件,方便触发。计算两个小数字段相乘并四舍五入保留2位小数,结果填入 文本a 字段。如下图:
3. 进入运行时,新建一张单据,给两个小数字段分别填入3025、0.033,然后随便改一下 文本1 字段的值触发事件,计算结果为 99.83。
计算一下看是否正确:
Python2的round函数是四舍五入,所以应该是99.83,正确。
4. 然后保存单据,关闭再打开,修改一下 文本1 字段触发事件,计算结果为99.82。这就不对了。
5. 再试试,修改3025为3000,修改一下 文本1 字段触发事件。然后再改回3025,然后再修改一下 文本1 字段触发事件,计算结果为99.83。看一下http数据:
可见传去服务端的是3025.0。
6. 关掉再打开单据,看看打开单据时服务端传来的值是多少:
可见服务端的值是3025.0000000000。这是因为数据库字段是decimal(23,10),所以数据库中存的就是这个。
7. 那么,问题就变成了,为什么3025.0000000000算出来就是99.82。
排查
1. BOS处理python表达式用的是第三方组件IronPython (2.6.10920.0),写个测试程序看看,代码如下:
简单地调IronPython,计算round( a * b, 2)的值。
运行结果:
可见,当3025后面有10个小数0时,计算结果99.82,不正确。当后面有8个小数0时,或没有0时,计算结果99.83,正确。多试几种情况发现,小数的位数为9、10时,结果为99.82,不正确。小数的位数为0-8时,结果为99.83,正确。而且结果类型是double。
2. 再把代码中的表达式改为a*b,看看运行结果:
都是99.825后面若干个0,结果类型是decimal。
3. 再改一下,直接计算round(a,2):
可见,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后四舍五入结果是多少:
断点调试看一看:
可见decimal的99.82500000000000000000转double后变成了99.824999999999989,那么四舍五入自然就变成了99.82。
少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)
这样先减少无效小数位数再计算,精度问题会少一些。