问题出自一个价格的转换需要将double类型转换(元)为integer类型(单位分)的数.
$salePrice = 10.2;
$price =(int) ($salePrice * 100); // 1019
虽然少了一分钱,但是对于这种问题的严重性,每个用户少付款一分钱,虽然说平台用户少,但对于对账会触发报警,甚至不能下单因为实际付款额与售价不等.
接下来我们先从计算机如何存储二进制数字说起.
整数部分 --> 转为二进制
小数点不动
小数部分 --> 转为二进制
这里重点说下:小数部分转换
0.25转换为二进制如下:
0.25 * 2 = 0.5 // 0 取整数位如果为1取1,这里为0取0,小数部分不为0所以 拿0.5去下一步
0.5*2 =1.0 // 1 小数部分为0 结束计算
10.25就可以表示为 1010.01
小数部分转换:
0.25
↓
0 1
得出小数部分算法
0*2^{-1} + 1*2^{-2}
众所周知,php使用C语言实现,浮点类型在zval中是double.
符号位 11位 指数 52位尾数
0 0000 0000 000 000.....000
以10.25为例:
1010.01 => 转换为
1.01001 * 2^{3}
中间数:为了表示负指数,所以提出了一个中间数的概念. 中间数:2^n-1 n为指数位数.
所以double类型的中间数为1023, 而float类型为127.
例如指数为-7那么, 127 + (-7)= 120 -> 01111000,读取时候还得减去127,得出真实值.
中间数可以理解为一个符号位,表示指数的符号.
中间数全为1,尾数位为全为0表示无穷大
中间数全为1,尾数位不全为0表示NaN
中间数全为0,尾数位全为0表示0
因此得到内存模型为:
符号位 11位 指数 52位尾数位
0 1000 0000 011 01001 0......000
从内存模型当中读取数据时:
符号位 11位 指数 52位尾数位
0 1000 0000 011 01001 0......000
+ 130-127 = 0000 0000 011= 3
尾数位前补1和. => 1.01001
将小数点右移指数位个数(减去中间数后如果指数位最低位为1则左移) => 1010.01
按照二进制转十进制规则得出:10.25
PHP中可以使用bcmath库,避免精度丢失.
两数相乘可以用bcmul这个函数.
(int)bcmul(10.2, 100) => 1020
使用bccomp准确比较两个任意精度类型.
其他语言参照相关的库,C语言中也有bcmath库的实现.
在涉及到跟金额相关的业务时候推荐最好这么做,避免产生类似不安全的问题.