处理精度丢之-如何解决

通过上篇我们了解到计算机是如何存储浮点数,那精度丢失是在哪产生的?

精度丢失场景

举个栗子: 0.1 + 0.2

0.1( 转化后是以0011无限循环,二进制为满一进一,所以末尾为01 )

转二进制后:0.0 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 01

0.2

转二进制后:0.0 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 0110 10

相加:0.0 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 11

工具转换为十进制: 0.30000000000000004

其实在四则运算中,浮点数转换成二进制后如果存在无限循环小数,都会有精度异常的情况

对于精度异常,该如何解决?

1、借助第三方引入npm包解决

Big.js

轻量,满足基本功能

npm install big.js --save

// Eg1: 加
let val = new Big(0.1)
let res = val.add(0.2)
console.log(res.toString()) // 0.3

// Eg2: 减
let val = new Big(0.2)
let res = val.minus(0.1)
console.log(res.toString()) // 0.1

// Eg3: 乘
let val = new Big(4)
let res = val.mul(3)
console.log(res.toString()) // 12

// Eg4: 除
let val = new Big(8)
let res = val.div(4)
console.log(res.toString()) // 2

// Eg5: 取余
let val = new Big(3)
let res = val.mod(2)
console.log(res.toString()) // 1

mathjs

内置功能丰富

npm install mathjs --save

创建一个带配置的mathjs实例
import { create, all } from 'mathjs'
const config = {
    number: 'number',
    precision: 16,
}
const math = create(all, config)

// Eg1: 基于原生的计算,格式化处理
let temp = 0.1 + 0.2
let result = math.format(temp, 14) // 0.3

// Eg2: 基于原生的计算,格式化处理保留2位小数
let temp = 0.1 + 0.2
let result = math.format(temp, {
    notation: 'fixed',
    precision: 2,
})  // 0.30

// Eg3: 基于包的大数计算(math实例配置此时也需调整 number: 'bignumber'), 格式化处理保留2位小数
let temp = math.add(math.bignumber(1000), math.bignumber(2000)
let result = math.format(temp, {
    notation: 'fixed',
    precision: 2,
}) // 3000.00

2、自定义方法

封装方法时存在一个容易踩的坑。我们知道浮点数的计算解决思路是转换为整数。那是不是直接将目标值各自乘以10的n次方转化为整数就可以。如:35.41,乘以100, 转化为整数应该就可。事实是:35.41 * 100 ==》 3540.9999999999995,因35.41转换为二进制的时候就丢失了精度,所以需要通过转换成字符串格式进行化整。

// 精确加法
function plus(num1, num2) {
  const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)))
  return (times(num1, baseNum) + times(num2, baseNum)) / baseNum
}
// 精确减法
function minus(num1, num2, ...others) {
  const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)))
  return (times(num1, baseNum) - times(num2, baseNum)) / baseNum
}
// 精确乘法
function times(num1, num2, ...others) {
  const num1Changed = float2Fixed(num1)
  const num2Changed = float2Fixed(num2)
  const baseNum = digitLength(num1) + digitLength(num2)
  const leftValue = num1Changed * num2Changed
  checkBoundary(leftValue)
  return leftValue / Math.pow(10, baseNum)
}
// 精确除法
function divide(num1, num2, ...others) {
  const num1Changed = float2Fixed(num1)
  const num2Changed = float2Fixed(num2)
  checkBoundary(num1Changed)
  checkBoundary(num2Changed)
  return times((num1Changed / num2Changed), Math.pow(10, digitLength(num2) - digitLength(num1)))
}
// 检测数字是否越界,如果越界给出提示
function checkBoundary(num) {
  if (_boundaryCheckingState)
    if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
      console.warn(`${num} 超出数字安全范围(${Number.MAX_SAFE_INTEGER},${Number.MIN_SAFE_INTEGER}),计算结果可能不准确`)
    }
  }
}
// 小数转整数
function float2Fixed(num) {
  num = num || 0
  if (num.toString().indexOf('e') === -1) {
    return Number(num.toString().replace('.', ''))
  }
  const dLen = digitLength(num)
  return dLen > 0 ? strip(num * Math.pow(10, dLen)) : num
}
// 小数点后的字符串长度
function digitLength(num) {
  num = num || 0
  const eSplit = num.toString().split(/[eE]/)
  const len = (eSplit[0].split('.')[1] || '').length - (+(eSplit[1] || 0))
  return len > 0 ? len : 0
}
// 错误数据转正常
function strip(num, precision = 12) {
  return +parseFloat(num.toPrecision(precision))
}

其他

1、toFixed存在丢失精度

大多数情况下, toFixed方法满足数学概念中的四舍五入,但它偶尔也会宕机,比如:1.335.toFixed(2) === 1.33

那咋办,只得自定义tofixed方法:

toFixedFn (val, n) {
    if (!n) { return Math.round(val) }
    val = Math.round(val * Math.pow(10, n)) / Math.pow(10, n)
    return val.toFixed(n)  (此处需要基于原生toFixed补0操作,如1.30保留两位小数时经过上面处理后为1.3)
}

// 知识补充:
Math.round(2.5) // 2 ( 把一个数字舍入为最接近的整数 )
Math.pow(10, 2) // 100 (返回 x 的 y 次幂)
posted @ 2020-11-09 23:42  Tiboo  阅读(923)  评论(0编辑  收藏  举报