遇到一个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是基于十进制的定点计算,所以不会产生精度误差
一个定点数包含了:用一个尾数(Mantissa)、一个基数(Base)、一个指数(Exponent)以及一个表示正负的符号(sign).
比如 15.99 用十进制科学计数法可以表达为 +1599 × 10⁻² ,其中 1.2345 为尾数,10 为基数,2 为指数。sign为 ‘+’。
具体的使用请查阅文档,本文仅做一个提醒,欢迎批评指正探讨
附带一个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();
}
}
}