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

遇到一个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();
    }
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 背景 在java中float赋值给double,会产生精度问题。 输出为2.0999999046325684。 小...
    我叫小小强阅读 19,206评论 2 23
  • 前言 在日常的开发中我们随时都会跟数字打着交道,对数字的处理也是很平常的事,本文仅对常用的数字操作一个小结,当一个...
    進无尽阅读 1,085评论 0 2
  • 浮点类型 用于表示有小数部分的数值。Java中有两种浮点类型, 也可以用16进制表示浮点数值。例如0.125=2-...
    狮_子歌歌阅读 1,245评论 0 2
  • floata =0.01;intb =99999999;doublec =0.0;c = a*b;NSLog(@"...
    Lv明阅读 1,065评论 0 0
  • 目录 1、八种基本数据类型 八种数据类型分别是:(整数型) byte,short,int,long,(浮点型)fl...
    AnchEvil阅读 1,577评论 0 0