IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    一张打折卡引发的精度思考

    Fish (fsh267@gmail.com)发表于 2016-08-11 00:00:00
    love 0

    1. 背景

    在验证网商营销平台时, 发现一个奇怪的打折卡

    Screen_Shot_2016-08-10_at_21.46.55

    经排查, 服务端返回的是打折卡额度分别是 0.39, 0.49, 前端数值乘以 10 进行展示, 0.39 * 10 = 3.9000000000000004, 在浏览器演示如下:

    Screen_Shot_2016-08-10_at_21.51.00

    很明显是 Javascript 浮点运算精度问题, 那么为什么两个运算结果一个正常, 一个这么不正经呢?

    2. Python 分析

    Javascript 和 Python 都是弱类型语言, 以0.1 + 0.2为例, 从更为熟悉的 Python 分析一下.

    在 CPython 解释器下, 得到的结果如下:

    Screen_Shot_2016-08-10_at_22.14.15

    我们知道计算机都是二进制运算, 通过进制转换工具, 或者文章末尾的 Python 计算函数get_float_bin(), 可以看到运算过程:

    0.1 的二进制: 0.0001100110011001100110011001100110011001100110011001101
    0.2 的二进制: 0.0011001100110011001100110011001100110011001100110011010
    二进制相加:	 0.0100110011001100110011001100110011001100110011001100111
    
    转换成浮点:   0.3000000000000000444089209850062616169452667236328125
    
    截断后:		 0.30000000000000004
    

    浮点数有指数和尾数组成, 在转换过程中, 无限长度的二进制数过程不可逆, 造成了精度丢失.

    至于上面的0.49 * 10 = 4.9, 可以推测, 相乘之后的二进制转成10进制, 恰好9后面有至少17个零, 这样 CPython 解释器就以4.9展示了.

    Python 眼中的 0.1

    通过 Decimal 模块, 我们看看 Python 是怎么看待 0.1 的, 比我们要费脑多了.

    >>> from decimal import Decimal
    >>> Decimal(0.1)
    Decimal('0.1000000000000000055511151231257827021181583404541015625')
    

    这个非常有意思, 也正好解答了许久以来的困惑, 为什么 Python(round)、Javascript(toFixed) 等语言的四舍五入不灵!!!

    • Python3
    >>> round(3.155, 2)
    3.15
    >>> round(2.615, 2)
    2.62
    
    • Node
    > console.log(2.675.toFixed(2))
    2.67
    undefined
    > 2.615.toFixed(2)
    '2.62'
    

    Python 和 Js 准守的是标准的四舍五入, 不存在(叫奇进偶舍), 至于为什么 3.155 没有五入, 我们看看 Python 是怎么看 3.155 的:

    >>> from decimal import Decimal
    >>> Decimal(3.155)
    Decimal('3.154999999999999804600747665972448885440826416015625')
    

    很显然, 他四舍五入取两位时, 肯定就是 3.15了!!

    3. 解决方案

    BB 了这么多, 吐槽一下解决方案:

    方案一: 统统转换成整数

    1. 二进制表示整数是没有精度丢失的, 浮点数的运算, 统一乘以 10 的 N 次方, 变成整数来运算

    方案二: BigDecimal

    1. 《Effective Java》 说了, 商业的计算, 要用 Java.math.BigDecimal, 前端就负责的渲染展示, 数值计算统统扔给服务端来保证吧!

    既然用到了 BigDecimal, 顺便提一下, 我们之前踩过一个坑, Double 类型直接转成 BigDecimal 类型去用:

    public class TestLab {
        public static void main(String[] args){
            Double num = 996.007;
            System.out.println(num);
            BigDecimal bgNum = new BigDecimal(num);
            System.out.println(bgNum);
        }
    }
    

    输出如下:

    996.007
    996.0069999999999481588019989430904388427734375 
    

    WTF! 正确的姿势应该是

    BigDecimal bgNum = new BigDecimal(num.toString());
    

    牢记 BigDecimal 只能精确的将字符串转成浮点!

    4. 其他

    1. Python 计算 float 的二进制

    from decimal import Decimal
    
    def get_float_bin(x, n=50):
        a = Decimal(x) * 2
        r = []
        for i in range(0, n):
            if a >= 1:
                r.append("1")
                a -= 1
            else:
                r.append("0")
            a = a * 2
        return "0." + "".join(r)
    
    // 结果
    >>> get_float_bin(0.1)
    '0.00011001100110011001100110011001100110011001100110'
    

    参考文档

    1. stackoverflow 问题
    2. Javascript中浮点数的计算精度问题
    3. 浮点数的精度

    希望 Wings 战队 Ti6 夺冠.


沪ICP备19023445号-2号
友情链接