美文网首页iOS OC 学习手册
【大数字精确计算】当心你的float、double类型产生误差

【大数字精确计算】当心你的float、double类型产生误差

作者: 码痞 | 来源:发表于2018-03-17 14:29 被阅读7次

    遇到一个Bug

    需求:在页面上显示一个产品的已购百分比
    
    已购百分比 = ((总量 - 剩余数量)/总量) * 100%
    显示结果向下取整(取两位有效数字),
    例如60.1% 则取60,60.9%也取60%
    

    然而当总额=4000 剩余数量=1680时,结果应该是正正好58%,
    但程序运行时向下取整后结果却是57%,
    我:???
    经过排查服务端取到的数据没有问题,上下文代码也没问题,那么问题出在哪儿呢。


    还不是因为我用了float类型

    在说这个bug之前我们来复习一下几个基础知识

    • 带小数位的十进制数如何转为二进制数
    • float / double 类型是如何存储的

    关于第一点大部分程序员都已经会了,简单的说就是小数部分乘2取整,不会的同学请自行百度一下,不占用篇幅,重点是小数部分换算二进制时,时常发生无限循环,例如0.1,大家可以自己尝试计算一下

    关于float和double的存储方式,以float举例(咱们挑和本篇问题有关的内容说

    float类型即浮点型,一个float占4个字节
    4个字节一共32位
    从左往右第1位表示这个数的正负(a)
    再往右的8个数位用于表示该浮点数存储的指数部分(b)
    最右边的23位表示这个数字的有效数位(c)

    正负表示位(1位) + 指数部分(8位) + 底数部分(23位) = 32位
    abbbbbbb / bccccccc / cccccccc / cccccccc

    然而23个底数位是不可能完整表示无限循环小数的


    精度不够,没东西凑

    一个无限循环,一个只有23位底数,那么结果是什么?
    精度丢失,23位之后的数字无法表示,被当作0处理了
    看回到我的bug上

    当总额=4000 剩余数量=1680时,结果应该是正正好58%,
    但程序运行时向下取整后结果却是57%

    经过计算

    (4000 - 1680) / 4000 = 0.58
    而0.58转为2进制是无限循环的小数
    float类型取了23个有效数位后,后面的数位给扔了……
    实际上计算的结果为 0.5799999999.....
    向下取整为 0.57
    0.57 * 100% = 57%

    原因就是如此,所以说啊,基本功是很重要的,


    那咋办呢?

    1.最简单的方法,不出现小数
    在这个bug里,最简单的修复方式是

    (4000 - 1680) / 4000 * 100
    改为
    (4000 - 1680) * 100 / 4000

    即将小数化为百分数的乘以100提前,使得小数不出现,就不会存在无限循环数了(事实证明这样确实有效)
    但这只仅限于使用百分比的情况,如果是一个大额度交易的精确计算,需要用以下第2点的方法

    2.有的时候出现小数是不可避免的,而同时需要非常高的精度,不容误差(金钱、折扣方面的计算);
    那么这个时候,就要掏出我们针对float/double类型的计算工具了 —— NSDecimalNumber

    NSDecimalNumber是基于十进制的定点计算,所以不会产生精度误差

    image.png

    一个定点数包含了:用一个尾数(Mantissa)、一个基数(Base)、一个指数(Exponent)以及一个表示正负的符号(sign).
    比如 15.99 用十进制科学计数法可以表达为 +1599 × 10⁻² ,其中 1.2345 为尾数,10 为基数,2 为指数。sign为 ‘+’。

    来源:NSDecimalNumber的介绍和使用

    具体的使用请查阅文档,本文仅做一个提醒,欢迎批评指正探讨


    附带一个JAVA的小函数,用于将数字展开为double类型的存储格式
    (写这篇博文的时候找不到这段代码的出处了,看到出处的朋友请告诉我一下我附上链接)

    public class showDouble {
      public static void main(String[] args) {
        printBits(3.54);
    
      }
    
      private static void printBits(double d) {
        System.out.println("##"+d);
        long l = Double.doubleToLongBits(d);
        String bits = Long.toBinaryString(l);
        int len = bits.length();
        System.out.println(bits+"#"+len);
        if(len == 64) {
            System.out.println("[63]"+bits.charAt(0));
            System.out.println("[62-52]"+bits.substring(1,12));
            System.out.println("[51-0]"+bits.substring(12, 64));
        } else {
            System.out.println("[63]0");
            System.out.println("[62-52]"+ pad(bits.substring(0, len - 52)));
            System.out.println("[51-0]"+bits.substring(len-52, len));
        }
      }
    
      private static String pad(String exp) {
        int len = exp.length();
        if(len == 11) {
            return exp;
        } else {
            StringBuilder sb = new StringBuilder();
            for (int i = 11-len; i > 0; i--) {
                sb.append("0");
            }
            sb.append(exp);
            return sb.toString();
        }
      }
    }

    相关文章

      网友评论

        本文标题:【大数字精确计算】当心你的float、double类型产生误差

        本文链接:https://www.haomeiwen.com/subject/cevrnxtx.html