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

QueXuQ · 发布于 2014年01月02日 · 最后由 ywjno 回复于 2014年01月03日 · 4384 次阅读
3547

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

0.8*0.2=0.16000000000000003

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

共收到 20 条回复
96

0.16000000000000003.toFixed(2)

1342

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

775

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

4002

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

3547

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

1342

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

3

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

1342

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

3

#8楼 @ywjno 这个算不算?

C4522b

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

1342

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

1342

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

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

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

3

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

3547

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

1107

#14楼 @QueXuQ

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

2880

#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')
2880

浮点数的计算机表示, 大一就应该学过吧...

另外有效数字多的才比较精确, 所以 0.16000000000000003 比 0.16 精确才对. 楼主想要的其实并不是精确结果, 而是适合阅读和记诵的结果.

3547

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

96

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)

1342

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

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