只想写个求和: 从0.1+0.2!==0.3 到 1.001*1000!==1001 的心酸史

177 阅读3分钟

吐槽:原本知识想写个求和的封装,结果0.1 + 0.2 !== 0.3 // 这是在js中遇到的很神奇也很烦的问题 到后来用扩大相同倍数成整数再相加又遇到1.001*1000!==1001,不得不得加法好难

Tip:想要结果的直接请调到最后!

原因

首先我们需要知道:

  1. Javascript采用了IEEE-745浮点数表示法
  2. 在计算机中都是以二进制进行存储

所以我们再来看看:0.1 + 0.2

先把 0.1 和 0.2 转换成二进制看看: 0.1 => 0.0001 1001 1001 1001…(无限循环) 0.2 => 0.0011 0011 0011 0011…(无限循环)

而双精度浮点数的小数部分最多支持 52 位,所以两者相加之后得到这么一串 0.0100110011001100110011001100110011001100110011001100 因浮点数小数位的限制而截断的二进制数字,这时候,我们再把它转换为十进制,就成了 0.30000000000000004

结果就会产生在控制台打印:0.1 + 0.2 === 0.3 会得到 false

首先想到的是将所有小数均乘10的备注,从而转为整数再相加,所以有了第一版解决方案

解决方案(假的)

基本都是将浮点数全部转为整数后相加,再对相加后的数除以10的n次方进行还原, 以下进行了简单的封装,需求不同的朋友可以参考进行别的扩展

  let sum1 = [0.01, 0.2] // 直接 0.01 + 0.2 会得到 0.21000000000000002
   /**
    * 对浮点数求和
    * @param {Array} [sumArr] 加数数组
    * @return {Number} 返回加数数组相加之和
    * */
    function sum(sumArr) {
      // 获取所有加数的小数位数
      let floatArr = sumArr.map(numItem => numItem.toString().split('.')[1] ? numItem.toString().split('.')[1].length :
        0)
      // 获取所有加数中小数位数最长的位数
      let maxFloat = Math.max(...floatArr)
      // 根据最长位数得到新的加数数组
      let newSumArr = sumArr.map(numItem => numItem * 10 ** maxFloat)
      // 计算出新的加数数组之和
      let sumNumber = 0
      newSumArr.forEach(num => {
        sumNumber += num
      });
      return sumNumber / 10 ** maxFloat
    }
   console.log(sum(sum1)) // 0.21

本来以为按照加数的小数位数进行相乘相加会没有问题,但是直到我用[1.001, 2.002]参数进行测试打印的结果尽然为3.0029999999999997,后来发现主要是因为在处理numItem * 10 ** maxFloat的时候,1.001*1000===1000.9999999999999说明乘以小数的位数暂时行不通,换个思路将加数转为整数字符串再相加减

解决方案(真的)

    function sum(sumArr) {
      // 获取所有加数的小数位数
      let floatArr = sumArr.map(numItem => numItem.toString().split('.')[1] ? numItem.toString().split('.')[1].length :
        0)
      // 获取所有加数中小数位数最长,最小的位数
      let maxFloat = Math.max(...floatArr)
      // 将所有数字转为不含小数点的整数(扩大相同倍数)
      let sumStrArr = sumArr.map((numItem, numIndex) => {
        // 转为不含小数点的整数
        let num = numItem.toString().replace('.', '') - 0
        // 保证所有数扩大相同倍数
        num *= 10 ** (maxFloat - floatArr[numIndex])
        return num
      })
      // 缩写
      // let sumStrArr = sumArr.map((numItem, numIndex) => (numItem.toString().replace('.', '') - 0) * 10 ** (maxFloat - floatArr[numIndex]))

      // 计算出新的加数数组之和
      let sumNumber = 0
      sumStrArr.forEach(num => {
        sumNumber += num
      });
      return sumNumber / 10 ** maxFloat
    }

测试:

sum([0.1, 0.2]) // 0.3
sum([1.001, 2.002]) // 3.003
sum([1, 2.002]) // 3.002
sum([1.01, 2.0002]) // 3.0102

解决方案(简化版)

这个简化版就要从一次找四舍五入的方法说起了,在javascript中四舍五入采用的是银行家算法,但是银行家算法并不是我们日常生活中所需要的四舍五入,找着找着然后的然后就发现了一个有趣的东西e,所以简化版的就出来了(有能更简洁的方法的话欢迎私聊)

function sum(sumArr) {
    // 获取所有加数的小数位数
    let floatArr = sumArr.map(numItem => numItem.toString().split('.')[1] ? numItem.toString().split('.')[1].length :
      0)
    // 获取所有加数中小数位数最长的位数
    let maxFloat = Math.max(...floatArr)
    // 将所有数字扩大相同倍数
    let sumStrArr = sumArr.map((numItem, numIndex) => Math.round(`${numItem}e${maxFloat}`))
    // 计算出新的加数数组之和
    let sumNumber = sumStrArr.reduce((num1, num2) => num1 + num2);
    return Number(`${sumNumber}e-${maxFloat}`)
  }