js数学计算精度溢出问题

902 阅读4分钟

1. js存在精度溢出的问题

例如,0.1+ 0.2 = 0.30000000000000004 1.gif

参考文章:JS中数字(精度缺失)存储问题

这篇文章主要是从二进制方向来讲解单精度浮点数和双精度浮点数的区别,从而延伸到js精度的问题

2. 解决问题

  • 手写方案,简单的加减乘法计算,主要原理:是通过将小数转化整数进行计算后还原小数。
/*
 * @Author: Null
 * @Date: 2021-11-16 11:49:15
 * @Description: 解决js小数点溢出的问题
 */
let floatTool = {
	math:function(){
		
		/*
		 * 判断obj是否为一个整数
		 */
		function isInteger(obj) {
			return Math.floor(obj) === obj
		}
		
		/*
		 * 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100
		 * @param floatNum {number} 小数
		 * @return {object}
		 *   {times:100, num: 314}
		 */
		function toInteger(floatNum) {
			var ret = {
				times: 1,
				num: 0
			}
			if (isInteger(floatNum)) {
				ret.num = floatNum
				return ret
			}
			var strfi = floatNum + ''
			var dotPos = strfi.indexOf('.')
			var len = strfi.substr(dotPos + 1).length
			var times = Math.pow(10, len)
			var intNum = parseInt(floatNum * times + 0.5, 10)
			ret.times = times
			ret.num = intNum
			return ret
		}
		
		/*
		 * 核心方法,实现加减乘除运算,确保不丢失精度
		 * 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
		 *
		 * @param a {number} 运算数1
		 * @param b {number} 运算数2
		 * @param digits {number} 精度,保留的小数点数,比如 2, 即保留为两位小数
		 * @param op {string} 运算类型,有加减乘除(add/subtract/multiply/divide)
		 *
		 */
		function operation(a, b, op) {
			var o1 = toInteger(a)
			var o2 = toInteger(b)
			var n1 = o1.num
			var n2 = o2.num
			var t1 = o1.times
			var t2 = o2.times
			var max = t1 > t2 ? t1 : t2
			var result = null
			switch (op) {
				case 'add':
					if (t1 === t2) { // 两个小数位数相同
						result = n1 + n2
					} else if (t1 > t2) { // o1 小数位 大于 o2
						result = n1 + n2 * (t1 / t2)
					} else { // o1 小数位 小于 o2
						result = n1 * (t2 / t1) + n2
					}
					return result / max
				case 'subtract':
					if (t1 === t2) {
						result = n1 - n2
					} else if (t1 > t2) {
						result = n1 - n2 * (t1 / t2)
					} else {
						result = n1 * (t2 / t1) - n2
					}
					return result / max
				case 'multiply':
					result = (n1 * n2) / (t1 * t2)
					return result
				case 'divide':
					return result = function() {
						var r1 = n1 / n2
						var r2 = t2 / t1
						return operation(r1, r2, 'multiply')
					}()
			}
		}
		
		// 加减乘除的四个接口
		function add(a, b) {
			return operation(a, b, 'add')
		}
		
		function subtract(a, b) {
			return operation(a, b, 'subtract')
		}
		
		function multiply(a, b) {
			return operation(a, b, 'multiply')
		}
		
		function divide(a, b) {
			return operation(a, b, 'divide')
		}

		// exports
		return {
			add: add,
			subtract: subtract,
			multiply: multiply,
			divide: divide
		}
	}
}

export default floatTool
  • 第三方库有 math.js、decimal.js、bignum、bigint 等。 这里不做过多讲解。下面主要讲解math.js

  • 使用 math.js 解决

mathJs官方文档:mathjs.org/

yarn add mathJs

参考配置

    import { create, all } from "mathjs";
    const config = {
      epsilon: 1e-12,
      matrix: "Matrix",
      number: "BigNumber", // 可选值:number BigNumber , BigNumber可以解决js小数点精度问题,而number不可行
      precision: 64,
      predictable: false,
      randomSeed: null,
    };
    const math = create(all, config);
    Vue.prototype.$math = math;

在页面中直接调用 this.$math

    console.group('mathJS处理前端精度问题')
    // 当我们对config进行配置并使用了之后,
    console.log('0.1+0.2 ' , 0.1+0.2) // 0.30000000000000004

    console.log('0.1+0.2 -- math.add()方法直接调用,存在的精度溢出问题' ,this.$math.add(0.1,0.2)) // 0.30000000000000004

    // add()方法解决 0.1+0.2 精度问题
    const ans1 = this.$math.add(0.1, 0.2)  
    console.log('0.1+0.2 -- config配置引入,处理精度的正确方法2' ,this.$math.format(ans1, {precision: 14})) // '0.3'


    // 当我们使用了evaluate()方法,需要进行mathJs自带的格式化方法format()进行格式化,转为字符串
    console.log('0.1+0.2 -- mathJs的evaluate方法' ,this.$math.evaluate('0.1+0.2'))  // Decimal {s: 1, e: -1, d: Array(1), constructor: ƒ}

    // 正确的使用方法
    console.log('0.1+0.2 -- config配置引入,evaluate()方法处理精度的正确方法3' ,this.$math.format(this.$math.evaluate('0.1+0.2'))) // 0.3

    // 其余示例
    console.log('其余示例 -- PI常量' , this.$math.format(this.$math.pi)) // 3.14159265
    console.log('其余示例 -- e常量' , this.$math.format(this.$math.e)) // 2.7182818
    console.log('其余示例 -- 加减乘除' ,this.$math.format(this.$math.evaluate('1.2 * (2 + 4.5)'))) // 7.8
    console.log('其余示例 -- 科学计算法' ,this.$math.format(this.$math.evaluate('1000*1000')) ) // 1e+6
    console.log('其余示例 -- 表达式计算' , this.$math.format(this.$math.evaluate('9 / 3 + 2i'))) // 3 + 2i

    console.log('其余示例 -- 表达式计算' , this.$math.format(this.$math.derivative('x^2 + x', 'x'))) // 2*x+1


    // 这里我不知道对不对
    console.log('其余示例 -- 矩阵计算' , this.$math.format(this.$math.evaluate('det([-1, 2; 3, 1])'))) // -7

    // 正余弦计算 该值实际上应该是约等于 0.5 .实际值为0.4999999999999999  mathJs官网的demo中测试得出
    console.log('其余示例 -- 正余弦计算' , this.$math.format(this.$math.evaluate('sin(45 deg) ^ 2'))) // config配置的precision: 64 ,保留64位小数  0.4999999999999999999999999999999999999999999999999999999999999999

    console.log('其余示例 -- 单位转换' , this.$math.format(this.$math.evaluate('12.7 cm to inch'))) // 5 inch

    console.log('其余示例 -- log计算' , this.$math.format(this.$math.log(10000, 10))) // 4

    console.log('其余示例 -- 开平方计算' , this.$math.format(this.$math.sqrt(-4))) // 2i

    // (3+4)*2
    console.log('其余示例 -- mathJs链式操作计算(3+4)*2' , this.$math.format(math.chain(3).add(4).multiply(2).done())) // 14

    console.groupEnd()

使用方法

  • 函数调用法:math.add(math.sqrt(4), 2)
  • 表达式法: math.eval('sqrt(4) + 2')
  • 链接操作法:math.chain(4).sqrt().add(2)

math.config 配置参数介绍

  • epsilon:用于测试两个比较值之间相等性的最小相对差异。所有关系函数都使用此值。默认值是1e-14。
  • matrix:函数的默认矩阵输出类型。
  • number:函数的数字输出类型,无法从输入中确定数字类型。但是对于大多数函数,输出的类型是根据输入确定的:作为输入的数字将返回一个数字作为输出,BigNumber作为输入返回BigNumber作为输出。
  • precision:BigNumbers的最大有效位数。此设置仅适用于BigNumbers,而不适用于数字。默认值是64。
  • predictable:可预测的输出类型的函数。如果为true,则输出类型仅取决于输入类型。如果为false(默认),则输出类型可能因输入值而异。例如math.sqrt(-4)返回complex('2i')时,可预见的是假的,而返回NaN时真。在以编程方式处理计算结果时可能需要可预测的输出,但在评估动态方程时可能对用户不方便。
  • randomSeed:将此选项设置为种子伪随机数生成,使其成为确定性的。每次设置此选项时,将使用提供的种子重置伪随机数生成器。例如,将其设置为'a'将导致math.random()返回0.43449421599986604每次设置选项后的首次通话。设置为null使用随机种子为伪随机数生成器设定种子。默认值是null。

常用方法

  • math.sqrt(4) 开方
  • math.add()
  • math.subtract()
  • math.divide()
  • math.multiply()