新手问题 js 的浮点数运算,如何最有效的保留小数?

QueXuQ · 2014年01月02日 · 最后由 ywjno 回复于 2014年01月03日 · 8101 次阅读

因为 js 没有像 ruby 的 decimal,只有浮点数,所有前台运算容易出现问题。就是精度问题,如:

0.8*0.2=0.16000000000000003

如果我只要保留两位小数,不知道朋友们是怎么解决这个问题的呢?

0.16000000000000003.toFixed(2)

decimal 的精度一样会存在问题,还是 casio 计算器那样的算得准

如果是 nodejs,有很多 npm 提供,如果是浏览器里,没有办法。

好办啊,Math.round(0.8 * 0.2 * 100) / 100

#2 楼 @ywjno decimal 的精度有问题,意味着只能用 integer? #1 楼 @johny #4 楼 @libuchao 这样的写法不知道会不会存在 bug 的。 网上找过很多相关信息,前端目测,没有哪个完美的方法,就如 #3 楼 @nouse 所说的那样。

#5 楼 @QueXuQ 浮点数计算本来进度就不行,要想计算得准确一个就是 casio 计算器,另一个就是 excel 的计算公式,java 的 BigDecimal 都不行,其他语言未知

#6 楼 @ywjno 很好奇,Casio 计算器能计算得准确浮点数?

#7 楼 @lgn21st 掏出计算器计算这个算式1/3*3

#8 楼 @ywjno 这个算不算?

我觉得应该有准确的前端库的,这又不是什么高技术含量的事情。

#9 楼 @lgn21st 你该换个计算器了,

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.363631248474B1单元格输入 =A1*1024*1024,得到结果 362140015 2)在 A2单元格输入449.179866790771B2单元格输入 =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

把上一个单元格计算正确的数字变成文本类型的时候,居然就又不正确了,这到底是怎么回事,有谁能解释一下么?

计算器计算正确的背后,它到底用了什么东西能让浮点数运算正确?

#11 楼 @ywjno 你这个,很强大!

#12 楼 @ywjno 这个太强了,言下之意是只有 integer 只能确保精度的准确吗? 浮点数的加减目测没问题,问题就在乘除上。

#14 楼 @QueXuQ

[1] pry(main)> ([0.1]*10).reduce &:+
=> 0.9999999999999999

#12 楼 @ywjno @lgn21st

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 精确才对。楼主想要的其实并不是精确结果,而是适合阅读和记诵的结果。

#16 楼 @luikore Woo.又学到东西了。是的,我不想用科学的精度结果,所以才想了解前端 js 里面有没有特别好的解决办法。

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)

#16 楼 @luikore 多谢吕大大的回答, 345.363631248474 是某第三方系统导出的 excel 里面的数据,因为要求数据要跟 excel 公式计算一致,所以这个细节完全就给忽视了。。。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号