Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

慎用Number.toFixed() #95

Open
ljianshu opened this issue Apr 22, 2021 · 4 comments
Open

慎用Number.toFixed() #95

ljianshu opened this issue Apr 22, 2021 · 4 comments

Comments

@ljianshu
Copy link
Owner

引言

最近在公司项目中碰到一个隐藏的bug,调试许久才发现竟然是toFixed函数精度问题引起的,从而引发了我一系列的思考。

我们都知道,计算机在二进制环境下,浮点数的计算精度会存在缺失问题,最经典的例子就是为什么0.1+0.2不等于0.3?

image.png

遇到上述问题,我们自然而然会想到toFixed方法来四舍五入,可结果却差强人意!

toFixed()的精度问题

我们来看一下toFixed在chrome、火狐、IE浏览器下的不同表现:

image.png

可以看到toFixed的四舍五入在chrome、火狐上并不准确。
toFixed在chrome、火狐上也并不是网上流传甚广的用银行家舍入法来进行四舍五入的。

银行家舍入法的规则是“四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一”。

例如银行家舍入法在 (2.55).toFixed(1) = 2.5、(3.55).toFixed(1) = 3.5 上就不符合了。

翻阅ecmascript规范toFixed的表述如下:

Number.prototype.toFixed

上面规范这段大概意思就是如果toFixed的入参小于10的21次方,那么就取一个整数n,让n*10^f - x 的精确值尽可能的趋近于0,如果存在两个这样的n,取较大的n。这段话可能有点晦涩难懂,我们举个例子比如 1.335.toFixed(2)

image.png

上图例子中1.335.toFixed(2)按照四舍六入五成双应该是1.34,但是实际情况确实1.33。这是因为n=133的时候让n*10^f - x更趋近于0,所以最后得到的结果是1.33。

解决方法

1.重写toFixed()

我们可以通过重写toFixed的方法,来实现四舍五入:

Number.prototype.toFixed = function (length) {
  var carry = 0 // 存放进位标志
  var num, multiple // num为原浮点数放大multiple倍后的数,multiple为10的length次方
  var str = this + '' // 将调用该方法的数字转为字符串
  var dot = str.indexOf('.') // 找到小数点的位置
  if (str.substr(dot + length + 1, 1) >= 5) carry = 1 // 找到要进行舍入的数的位置,手动判断是否大于等于5,满足条件进位标志置为1
  multiple = Math.pow(10, length) // 设置浮点数要扩大的倍数
  num = Math.floor(this * multiple) + carry // 去掉舍入位后的所有数,然后加上我们的手动进位数
  var result = num / multiple + '' // 将进位后的整数再缩小为原浮点数
  /*
   * 处理进位后无小数
   */
  dot = result.indexOf('.')
  if (dot < 0) {
    result += '.'
    dot = result.indexOf('.')
  }
  /*
   * 处理多次进位
   */
  var len = result.length - (dot + 1)
  if (len < length) {
    for (var i = 0; i < length - len; i++) {
      result += 0
    }
  }
  return result
}

该方法的大致思路是首先找到舍入位,判断该位置是否大于等于5,条件成立手动进一位,然后通过参数大小将原浮点数放大10的参数指数倍,然后再将包括舍入位后的位数利用Math.floor全部去掉,根据我们之前的手动进位来确定是否进位。

2.high-precision-four-fundamental-rules

在GitHub上找到一个高精度的基本四则运算npm包,用来弥补原生JS中toFixed方法计算精度缺失的不足,该作者用四舍五入算法重写了改方法,并封装成npm包!

// 安装
$ npm install high-precision-four-fundamental-rules --save
// 使用
import {add, subtract, multiply, divide} from 'high-precision-four-fundamental-rules';
add(1, 2, 4); // '3.0000'
subtract(1, 2, 3); // '-1.000';
multiply(1, 2, 2); // '2.00';
divide(1, 3, 7); // '0.3333333';

参考文章

@gdutwyg
Copy link

gdutwyg commented Dec 20, 2022

image
你好,这个怎么解释呢?如果n一样,取较大的那个,但是实际是取较小的那个

@saradean90
Copy link

saradean90 commented Mar 27, 2023

Thanks for the information.. https://www.mygeorgiasouthern.org/

@xiaokunxu
Copy link

xiaokunxu commented Mar 27, 2023 via email

@Kerry12342
Copy link

Kerry12342 commented Feb 29, 2024

When I tried to use IEEE 754 MIPS single precision representation (1-bit sign, 8-bit exponent, 23-bit fraction) to store the floating point decimal into binary and then convert back to decimal, if the last 3 bits of the fraction field are recognized as guard, round, and sticky, the calculated result is 1.334, which would round down to 1.33 like it showed up in your example.

I haven't test it so this is just a thought but would creating an algorithm with threshold for deciding which precision representation to use (use a larger precision when needed) to store the decimal be another way to solve this problem?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants