JavaScript 精度丧失题目

// 1. 两数相加
// 0.1 + 0.2 = 0.30000000000000004
// 0.7 + 0.1 = 0.7999999999999999
// 0.2 + 0.4 = 0.6000000000000001
// 2.22 + 0.1 = 2.3200000000000003

// 2. 两数相减
// 1.5 - 1.2 = 0.30000000000000004
// 0.3 - 0.2 = 0.09999999999999998

// 3. 两数相乘
// 19.9 * 100 = 1989.9999999999998
// 19.9 * 10 * 10 = 1990
// 1306377.64 * 100 = 130637763.99999999
// 1306377.64 * 10 * 10 = 130637763.99999999
// 0.7 * 180 = 125.99999999999999

// 4. 不一样的数却相称
// 1000000000000000128 === 1000000000000000129

盘算机的底层实现就没法完整准确示意一个无穷轮回的数,而且能够存储的位数也是有限定的,所以在盘算过程当中只能舍去过剩的部份,获得一个只管靠近实在值的数字示意,因而造成了这类盘算误差。

比方在 JavaScript 中盘算0.1 + 0.2时,十进制的0.1和0.2都会被转换成二进制,但二进制并不能完整准确示意转换效果,由于效果是无穷轮回的。

// 百度进制转换东西
0.1 -> 0.0001100110011001...
0.2 -> 0.0011001100110011...

In JavaScript, Number is a numeric data type in the double-precision 64-bit floating point format (IEEE 754). In other programming languages different numeric types can exist, for examples: Integers, Floats, Doubles, or Bignums.

依据 MDN这段关于Number的形貌 能够得知,JavaScript 里的数字是采纳 IEEE 754 范例的 64 位双精度浮点数。该范例定义了浮点数的花样,最大最小局限,以及凌驾局限的舍入体式格局等范例。所以只要不凌驾这个局限,就不会存在舍去,也就不会存在精度题目了。比方:

// Number.MAX_SAFE_INTEGER 是 JavaScript 里能示意的最大的数了,超出了这个局限就不能保证盘算的准确性了
var num = Number.MAX_SAFE_INTEGER;
num + 1 === num +2 // = true

现实工作中我们也用不到这么大的数或者是很小的数,也应当只管把这类对精度请求高的盘算交给后端去盘算,由于后端有成熟的库来处理这个盘算题目。前端虽然也有相似的库,然则前端引入一个如许的库价值太大了。

消除直接运用的数太大或太小超出局限,涌现这类题目的状况基本是浮点数的小数部份在转成二进制时丧失了精度,所以我们能够将小数部份也转换成整数后再盘算。网上许多帖子贴出的处理方案就是这类:

var num1 = 0.1
var num2 = 0.2
(num1 * 10 + num2 * 10) / 10 // = 0.3

然则如许转换整数的体式格局也是一种浮点数盘算,在转换的过程当中就可能存在精度题目,比方:

1306377.64 * 10 // = 13063776.399999999
1306377.64 * 100 // = 130637763.99999999
var num1 = 2.22;
var num2 = 0.1;
(num1 * 10 + num2 * 10) / 10 // = 2.3200000000000003

所以不要直接经由过程盘算将小数转换成整数,我们能够经由过程字符串操纵,挪动小数点的位置来转换成整数,末了再一样经由过程字符串操纵转换回小数:

/**
 * 经由过程字符串操纵将一个数放大或减少指定倍数
 * @num 被转换的数
 * @m   放大或减少的倍数,为正示意小数点向右挪动,示意放大;为负反之
 */
function numScale(num, m) {
  // 拆分整数、小数部份
  var parts = num.toString().split('.');
  // 原始值的整数位数
  const integerLen = parts[0].length;
  // 原始值的小数位数
  const decimalLen = parts[1] ? parts[1].length : 0;
  
  // 放大,当放大的倍数比本来的小数位大时,须要在数字背面补零
  if (m > 0) {
    // 补多少个零:m - 原始值的小数位数
    let zeros = m - decimalLen;
    while (zeros > 0) {
      zeros -= 1;
      parts.push(0);
    }
  // 减少,当减少的倍数比本来的整数位大时,须要在数字前面补零
  } else {
    // 补多少个零:m - 原始值的整数位数
    let zeros = Math.abs(m) - integerLen;
    while (zeros > 0) {
      zeros -= 1;
      parts.unshift(0);
    }
  }

  // 小数点位置,也是整数的位数: 
  //    放大:原始值的整数位数 + 放大的倍数
  //    减少:原始值的整数位数 - 减少的倍数
  var index = integerLen + m;
  // 将每一位都拆到数组里,轻易插进去小数点
  parts = parts.join('').split('');
  // 当为减少时,由于可能会补零,所以运用原始值的整数位数
  // 盘算出的小数点位置可能为负,这个负数应当正好是补零的
  // 个数,所以小数点位置应当为 0
  parts.splice(index > 0 ? index : 0, 0, '.');

  return parseFloat(parts.join(''));
}
/**
 * 猎取小数位数
 */
function getExponent(num) {
  return Math.floor(num) === num ?
    0 : num.toString().split('.')[1].length;
}

/**
 * 两数相加
 */
function accAdd(num1, num2) {
  const multiple = Math.max(getExponent(num1), getExponent(num2));
  return numScale(numScale(num1, multiple) + numScale(num2, multiple), multiple * -1);
}

测试用例:

describe('accAdd', function() {
  it('(0.1, 0.2) = 0.3', function() {
    assert.strictEqual(0.3, _.accAdd(0.1, 0.2))
  })
  it('(2.22, 0.1) = 2.32', function() {
    assert.strictEqual(2.32, _.accAdd(2.22, 0.1))
  })
  it('(11, 11) = 22', function() {
    assert.strictEqual(22, _.accAdd(11, 11))
  })
})
    原文作者:xiaoyann
    原文地址: https://segmentfault.com/a/1190000007649282
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞