因为 js 没有像 ruby 的 decimal,只有浮点数,所有前台运算容易出现问题。就是精度问题,如:
0.8*0.2=0.16000000000000003
如果我只要保留两位小数,不知道朋友们是怎么解决这个问题的呢?
update1:下文有技术层面的错误,为了保证当初原文的完整此帖不做修改,请继续看下面的回帖
以下文字皆纪念某次通宵干到早上 5 点,在导入 excel 数据的时候发现的问题
首先我们来进行试验,掏出你的计算器吧,我用的是貌似中小学生标配的计算器 casio fx-991ES PLUS
,在里面
1)输入 345.363631248474*1024*1024
,得到结果362140015
2)输入 449.179866790771*1024*1024
,得到结果470999228
注意,OS X 10.9 系统自带的计算器连1/3*3
都算不准的咱还是换一个计算器来玩吧,win xp 的计算器算这个算式都会得出正确答案
为了验证是否正确,接着打开一个类似 excel 的东东——我用的是 WPS 表格 (8.1.0.3526)
1)在 A1
单元格输入345.363631248474
,B1
单元格输入 =A1*1024*1024
,得到结果 362140015
2)在 A2
单元格输入449.179866790771
,B2
单元格输入 =A2*1024*1024
,得到结果 470999228
以上证明咱没有手误之类的问题
注意,xubuntu 自带的 Gnumeric 电子表格
计算是不准确的,Numbers 倒是能算正确,其他未知
接下来看看 ruby 的表现如何
require 'bigdecimal'
require 'bigdecimal/util'
result1 = BigDecimal.new("345.363631248474") * BigDecimal.new(1024*1024)
puts "here is 345.363631248474 result"
puts result1.to_s
puts result1.to_i
puts result1.to_f
puts result1.to_digits
puts '='*50
result2 = BigDecimal.new("449.179866790771") * BigDecimal.new(1024*1024)
puts "here is 449.179866790771 result"
puts result2.to_s
puts result2.to_i
puts result2.to_f
puts result2.to_digits
得到的结果是
here is 345.363631248474 result
0.362140014999999873024E9
362140014
362140014.9999999
362140014.999999873024
==================================================
here is 449.179866790771 result
0.470999227999999492096E9
470999227
470999227.99999946
470999227.999999492096
这段代码在版本 1.9.3 跟 2.1.0 运行的结果都一样。
用 java 写上面的计算的话也是同样结果,有兴趣的可以试试,我就不上代码了。
到这里基本可以看出计算浮点类型的差不多都是不准确了。
但是,excel 的公式就是计算准确的么?
我们接着在刚才那个 excel 文件里面做如下试验
1)在 C1
单元格输入=B1&""
,得到结果 362140015
2)在 C2
单元格输入=B2&""
,得到结果 470999227.999999
把上一个单元格计算正确的数字变成文本类型的时候,居然就又不正确了,这到底是怎么回事,有谁能解释一下么?
计算器计算正确的背后,它到底用了什么东西能让浮点数运算正确?
345.363631248474*1024*1024 的结果明显不该是整数啊...
小学生都能告诉你,用 bigdecimal 算出来的结果是精确的,你看 345.363631248474 带了 12 位小数,而最末位不是 5, 那不管乘以 1024 多少次,运算结果还是必须至少带 12 位小数且最后不是 0. 另外用 ruby 2.0 的有理数 (注意 r 后缀) 也显示分数不可约:
345.363631248474r * 1024 * 1024 #=> (88413089599609344/244140625)
其实 excel 也是用双精度浮点数,唯一区别就是,它取 64bit 时用了进位,而 ruby 取 64bit 时和其他编程语言一样用了截断。
我们可以做个实验,把 345.363631248474 用 80 bit 浮点数 (long double) 表示,然后看看每个字节
#include <stdio.h>
typedef union {
long double ieee754;
unsigned char binary[10];
} Converter;
int main (int argc, char const *argv[]) {
long double n = 345.363631248474L;
Converter c;
c.ieee754 = n;
// 看看每个字节
printf("%s", "bytes: ");
for (long i = 0; i < 10; i++) {
printf("%d,", c.binary[i]);
}
printf("%s", "\n");
// 四舍五入把低 16 位置 0
c.binary[0] = 0;
c.binary[1] = 0;
c.binary[2] = 0;
c.binary[3] = 0;
c.binary[4] ++;
printf("before: %.20Lf\n", n * 1024 * 1024);
printf("after: %.20Lf\n", c.ieee754 * 1024 * 1024);
return 0;
}
结果
bytes: 245,238,255,255,119,139,174,172,7,64,
before: 362140014.99999987301998771727
after: 362140015.00000000000000000000
另外注意 ruby 里的 to_i
是截断 (在 C 里用 (int)
强转浮点数到整数也是截断的,least surprise 原则), round
才是四舍五入的。
卡西欧也是用双精度浮点数,算下面这样的数结果一样是 0:
1E60 + 1 - 1E60
ruby bigdecimal 可以算出正确结果 1:
BigDecimal('1E60') + 1 - BigDecimal('1E60')
浮点数的计算机表示,大一就应该学过吧...
另外有效数字多的才比较精确,所以 0.16000000000000003 比 0.16 精确才对。楼主想要的其实并不是精确结果,而是适合阅读和记诵的结果。
IEEE 二進位浮點數算術標準(IEEE 754)http://zh.wikipedia.org/wiki/IEEE_754 雙精度浮點數 http://zh.wikipedia.org/wiki/%E5%8F%8C%E7%B2%BE%E5%BA%A6 雙精度浮點數 (double) 使用 64 位(8 字节)來儲存一個浮點數。它可以表示十进制的 15 或 16 位有效数字 345.363631248474 已经 15 位了,执行乘法就超过了双精度浮点数的精度,肯定不会精确的,需要进行舍入。进行舍入 IEEE 有四种方法,不同的 cpu,操作系统,语言实现都可能不一样
捨入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even)(这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中式以0结尾的)。
朝+∞方向捨入:會將結果朝正無限大的方向捨入。
朝-∞方向捨入: 會將結果朝負無限大的方向捨入。
朝0方向捨入: 會將結果朝0的方向捨入。
BigDecimal 能正确精确计算浮点数 就实际需求来说,没有必要用 BigDecimal,对精度要求最严格的系统应该是金融结算,其中每一步的算法都是固定的,A / B * C 写代码的时候就不能用 A * C / B, 并且 A / B 之后保留多少位小数都是有要求的。遇到这种需求,如果客户没有提到计算方法,可以跟客户讨论把算法及每一步的精度写在需求规格说明书中。
回到楼主的问题,可以用 1 楼的方法,toFixed(2)