JS精度问题总结

【JS 基础】JS 浮点数四则运算精度丢失问题 (3)
关于 JavaScript 浮点运算的精度解决方案

问题描述

1
2
3
4
5
6
7
8
示例代码:
    var x  = 0.3 - 0.2; //30美分减去20美分
    var y =  0.2 - 0.1; //20美分减去10美分
    x == y;             // =>false,两值不相等
    x == 0.1;           // =>false,真实值为:0.09999999999999998
    y == 0.1;           // =>true
    这个问题并不只是在Javascript中才会出现,任何使用二进制浮点数的编程语言都会有这个问题,只不过在 C++/C#/Java 这些语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。

产生原因

Javascript 采用了 IEEE-745 浮点数表示法(几乎所有的编程语言都采用),这是一种二进制表示法,可以精确地表示分数,比如 1/2,1/8,1/1024。遗憾的是,我们常用的分数(特别是在金融的计算方面)都是十进制分数 1/10,1/100 等。二进制浮点数表示法并不能精确的表示类似 0.1 这样 的简单的数字,上诉代码的中的 x 和 y 的值非常接近最终的正确值,这种计算结果可以胜任大多数的计算任务:这个问题也只有在比较两个值是否相等时才会出现。
javascript 的未来版本或许会支持十进制数字类型以避免这些舍入问题,在这之前,你更愿意使用大整数进行重要的金融计算,例如,要使用整数‘分’而不是使用小数‘元’进行货比单位的运算———以上整理自《Javascript 权威指南 P37》

0.1+0.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。

解决方案 (引自:解决方案

为了解决浮点数运算不准确的问题,在运算前我们把参加运算的数先升级(10 的 X 的次方)到整数,等运算完后再降级(0.1 的 X 的次方)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//加法
Number.prototype.add = function(arg) {
  var r1, r2, m;
  try {
    r1 = this.toString().split(".")[1].length;
  } catch (e) {
    r1 = 0;
  }
  try {
    r2 = arg.toString().split(".")[1].length;
  } catch (e) {
    r2 = 0;
  }
  m = Math.pow(10, Math.max(r1, r2));
  return (this * m + arg * m) / m;
};
//减法
Number.prototype.sub = function(arg) {
  return this.add(-arg);
};

//乘法
Number.prototype.mul = function(arg) {
  var m = 0,
    s1 = this.toString(),
    s2 = arg.toString();
  try {
    m += s1.split(".")[1].length;
  } catch (e) {}
  try {
    m += s2.split(".")[1].length;
  } catch (e) {}
  return (
    (Number(s1.replace(".", "")) * Number(s2.replace(".", ""))) /
    Math.pow(10, m)
  );
};

//除法
Number.prototype.div = function(arg) {
  var t1 = 0,
    t2 = 0,
    r1,
    r2;
  try {
    t1 = this.toString().split(".")[1].length;
  } catch (e) {}
  try {
    t2 = arg.toString().split(".")[1].length;
  } catch (e) {}
  with (Math) {
    r1 = Number(this.toString().replace(".", ""));
    r2 = Number(arg.toString().replace(".", ""));
    return (r1 / r2) * pow(10, t2 - t1);
  }
};

此方案只支持浮点数,若想支持浮点数整数混用,请自行调整。

终极解决方案

解决问题的思路,其实就是考虑在计算结果上保留几位小数的问题。

1
2
3
4
5
6
7
8
9
10
/**
 * 小数点后面保留第 n 位
 *
 * @param x 做近似处理的数
 * @param n 小数点后第 n 位
 * @returns 近似处理后的数
 */
function roundFractional(x, n) {
  return Math.round(x * Math.pow(10, n)) / Math.pow(10, n);
}
roundFractional(0.1 + 0.2, 1); // 结果:0.3

其他解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
//避免精度丢失的问题
/*
 * 判断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 };
  var isNegative = floatNum < 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(Math.abs(floatNum) * times + 0.5, 10);
  ret.times = times;
  if (isNegative) {
    intNum = -intNum;
  }
  ret.num = intNum;
  return ret;
}

/*
 * 核心方法,实现加减乘除运算,确保不丢失精度
 * 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
 *
 * @param a {number} 运算数1
 * @param b {number} 运算数2
 * @param digits {number} 精度,保留的小数点数,比如 2, 即保留为两位小数
 * @param op {string} 运算类型,有加减乘除(exAdd/exSubtract/exMultiply/exDivide)
 *
 */
function operation(a, b, digits, 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 "exAdd":
      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 "exSubtract":
      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 "exMultiply":
      result = (n1 * n2) / (t1 * t2);
      return result;
    case "exDivide":
      result = (n1 / n2) * (t2 / t1);
      return result;
  }
}

// 加减乘除的四个接口
function exAdd(a, b, digits) {
  return operation(a, b, digits, "exAdd");
}
function exSubtract(a, b, digits) {
  return operation(a, b, digits, "exSubtract");
}
function exMultiply(a, b, digits) {
  return operation(a, b, digits, "exMultiply");
}
function exDivide(a, b, digits) {
  return operation(a, b, digits, "exDivide");
}
// toFixed 修复
function exToFixed(num, s) {
  var times = Math.pow(10, s);
  var des = num * times + 0.5;
  des = parseInt(des, 10) / times;
  return des + "";
}
1
2
3
4
5
var ytje = parseFloat("0.03");
var handlecharge = parseFloat("0.01");
ytje = ytje - handlecharge;
ytje = new Number(ytje).toFixed(2);
console.log(ytje);