如何处理javascript中的浮点数精度?

How to deal with floating point number precision in JavaScript?

我有以下虚拟测试脚本:

1
2
3
4
5
function test(){
    var x = 0.1 * 0.2;
    document.write(x);
}
test();

这将打印结果0.020000000000000004,而只打印0.02(如果使用计算器)。据我所知,这是由于浮点乘法精度的错误造成的。

是否有人有一个好的解决方案,以便在这种情况下,我得到正确的结果0.02?我知道还有类似于toFixed的函数,或者舍入是另一种可能性,但是我真的希望打印整个数字而不进行任何切割和舍入。只是想知道你们中是否有人有一些好的,优雅的解决方案。

当然,否则我会四舍五入到10位左右。


从浮点指南:

What can I do to avoid this problem?

That depends on what kind of
calculations you’re doing.

  • If you really need your results to add up exactly, especially when you
    work with money: use a special decimal
    datatype.
  • If you just don’t want to see all those extra decimal places: simply
    format your result rounded to a fixed
    number of decimal places when
    displaying it.
  • If you have no decimal datatype available, an alternative is to work
    with integers, e.g. do money
    calculations entirely in cents. But
    this is more work and has some
    drawbacks.

注意,第一点只适用于您真正需要特定精确小数行为的情况。大多数人不需要它,他们只是对自己的程序不能正确地使用1/10这样的数字而感到恼火,因为他们没有意识到,如果1/3发生了同样的错误,他们甚至不会闪烁。

如果第一点真的适用于您,请使用bigdecimal作为javascript,这一点一点都不优雅,但实际上解决了问题,而不是提供不完美的解决方案。


我喜欢佩德罗·拉达里亚的解决方案,并使用类似的方法。

1
2
3
function strip(number) {
    return (parseFloat(number).toPrecision(12));
}

与Pedros解决方案不同,这将使0.999……重复,并且精确到最低有效数字的正负1。

注意:在处理32或64位浮点时,应该使用toprection(7)和toprection(15)以获得最佳结果。有关原因的信息,请参阅此问题。


数学倾向:http://docs.oracle.com/cd/e19957-01/806-3568/ncg_goldberg.html

建议的方法是使用校正因子(乘以适当的10次方,使算术发生在整数之间)。例如,在0.1 * 0.2的情况下,修正系数为10,您正在进行计算:

1
2
3
4
5
6
7
> var x = 0.1
> var y = 0.2
> var cf = 10
> x * y
0.020000000000000004
> (x * cf) * (y * cf) / (cf * cf)
0.02

一个(非常快速的)解决方案看起来像:

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
var _cf = (function() {
  function _shift(x) {
    var parts = x.toString().split('.');
    return (parts.length < 2) ? 1 : Math.pow(10, parts[1].length);
  }
  return function() {
    return Array.prototype.reduce.call(arguments, function (prev, next) { return prev === undefined || next === undefined ? undefined : Math.max(prev, _shift (next)); }, -Infinity);
  };
})();

Math.a = function () {
  var f = _cf.apply(null, arguments); if(f === undefined) return undefined;
  function cb(x, y, i, o) { return x + f * y; }
  return Array.prototype.reduce.call(arguments, cb, 0) / f;
};

Math.s = function (l,r) { var f = _cf(l,r); return (l * f - r * f) / f; };

Math.m = function () {
  var f = _cf.apply(null, arguments);
  function cb(x, y, i, o) { return (x*f) * (y*f) / (f * f); }
  return Array.prototype.reduce.call(arguments, cb, 1);
};

Math.d = function (l,r) { var f = _cf(l,r); return (l * f) / (r * f); };

在这种情况下:

1
2
> Math.m(0.1, 0.2)
0.02

我绝对推荐使用像sinfuljs这样经过测试的库


你只做乘法吗?如果是这样,那么你就可以利用一个关于十进制算术的绝妙的秘密。那就是江户十一〔0〕。也就是说,如果我们有0.123 * 0.12,那么我们知道会有5个小数位,因为0.123有3个小数位,0.12有2个小数位。因此,如果javascript给了我们一个像0.014760000002这样的数字,我们就可以安全地四舍五入到小数点后5位,而不必担心会失去精度。


您正在寻找一个Javascript的sprintf实现,这样您就可以以您期望的格式写出带有小错误的浮点(因为它们存储在二进制格式中)。

试试javascriptsprintf,你可以这样称呼它:

1
var yourString = sprintf("%.2f", yourNumber);

把你的数字打印成两位小数的浮点数。

如果您不希望只包含更多的文件用于浮点取整到给定精度,那么也可以将number.tofixed()用于显示目的。


我发现bignumber.js满足我的需求。

A JavaScript library for arbitrary-precision decimal and non-decimal arithmetic.

< /块引用>

它有很好的文档,作者非常努力地回应反馈。

同一作者还有两个类似的库:

Big.JS

A small, fast JavaScript library for arbitrary-precision decimal arithmetic. The little sister to bignumber.js.

< /块引用>

Decimal.js

An arbitrary-precision Decimal type for JavaScript.

< /块引用>

下面是一些使用bignumber的代码:

1
2
3
4
5
6
7
8
9
10
11
$(function(){

 
  var product = BigNumber(.1).times(.2);  
  $('#product').text(product);

  var sum = BigNumber(.1).plus(.2);  
  $('#sum').text(sum);


});
1
2
3
4
5
6
7
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">

<!-- 1.4.1 is not the current version, but works for this example. -->
<script src="http://cdn.bootcss.com/bignumber.js/1.4.1/bignumber.min.js">

.1 &times; .2 = <span id="product"></span>
.1 &plus; .2 = <span id="sum"></span>


此函数将从两个浮点数的乘法中确定所需的精度,并返回具有适当精度的结果。虽然不是很优雅。

1
2
3
4
5
function multFloats(a,b){
  var atens = Math.pow(10,String(a).length - String(a).indexOf('.') - 1),
      btens = Math.pow(10,String(b).length - String(b).indexOf('.') - 1);
  return (a * atens) * (b * btens) / (atens * btens);
}

1
2
3
var times = function (a, b) {
    return Math.round((a * b) * 100)/100;
};

---或者---

1
2
3
4
5
var fpFix = function (n) {
    return Math.round(n * 100)/100;
};

fpFix(0.1*0.2); // -> 0.02

---也---

1
2
3
4
5
6
7
8
9
10
var fpArithmetic = function (op, x, y) {
    var n = {
            '*': x * y,
            '-': x - y,
            '+': x + y,
            '/': x / y
        }[op];        

    return Math.round(n * 100)/100;
};

---就像---

1
2
3
4
5
6
7
8
9
10
11
fpArithmetic('*', 0.1, 0.2);
// 0.02

fpArithmetic('+', 0.1, 0.2);
// 0.3

fpArithmetic('-', 0.1, 0.2);
// -0.1

fpArithmetic('/', 0.2, 0.1);
// 2


你只需要决定你到底想要多少个小数点就行了——不能把蛋糕也吃了——)

数字错误随着每一次进一步的操作而累积,如果你不尽早切断它,它就会增长。数字库显示的结果看起来很干净,只需在每个步骤中切断最后2位数字,数字协处理器也有一个"正常"和"完整"的长度,原因相同。CUF-off对于处理器来说是很便宜的,但是对于脚本来说是非常昂贵的(乘法、除法和使用pov(…)。好的数学库将提供地板(x,n)来为您做截止。

因此,至少应该使用pov(10,n)使全局var/常数为-

1
Math.floor(x*PREC_LIM)/PREC_LIM  // floor - you are cutting off, not rounding

你也可以继续做数学运算,最后只做截止-假设你只显示结果,不做IF-S。如果可以这样做,那么.tofixed(…)可能更有效。

如果您正在进行if-s/比较,并且不想切掉它,那么您还需要一个小常量,通常称为eps,它比最大预期错误高一个小数位。假设你的截止值是最后两位小数,那么你的每股收益在最后一位的第三位是1(第三位最小显著),你可以用它来比较结果是否在预期的每股收益范围内(0.02-eps<0.1*0.2<0.02+eps)。


phpjs.org中的round()函数运行良好:http://phpjs.org/functions/round

1
2
num = .01 + .06;  // yields 0.0699999999999
rnum = round(num,12); // yields 0.07


令人惊讶的是,这个函数还没有发布,尽管其他函数也有类似的变化。它来自mdn web docs for math.round()。它简洁,允许不同的精度。

1
2
3
4
function precisionRound(number, precision) {
  var factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

console.log(precisionround(1234.5678,1));//预期输出:1234.6

console.log(precisionround(1234.5678,-1));//预期输出:1230

1
2
3
4
5
6
7
8
9
10
11
12
var inp = document.querySelectorAll('input');
var btn = document.querySelector('button');

btn.onclick = function(){
  inp[2].value = precisionRound( parseFloat(inp[0].value) * parseFloat(inp[1].value) , 5 );
};

//MDN function
function precisionRound(number, precision) {
  var factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}
1
2
3
button{
display: block;
}
1
2
3
4
<input type='text' value='0.1'>
<input type='text' value='0.2'>
<button>Get Product</button>
<input type='text'>


您得到的结果是正确的,并且在不同语言、处理器和操作系统中的浮点实现之间相当一致——唯一改变的是当浮点实际上是一个双精度(或更高)时的不准确程度。

二进制浮点数中的0.1类似于十进制的1/3(即0.333333333333…永远),没有准确的方法来处理它。

如果您处理的是浮点型,那么总是会出现小的舍入错误,因此您也必须将显示的结果舍入到一些合理的值。作为回报,您将得到非常快速和强大的算法,因为所有的计算都是在处理器的本机二进制中进行的。

大多数情况下,解决方案不是切换到定点算术,主要是因为它慢得多,99%的时间你只是不需要精度。如果你处理的东西确实需要那么高的准确性(例如金融交易),那么不管怎样,javascript可能不是最好的工具(因为你想强制使用固定点类型,静态语言可能更好)。

您正在寻找一个优雅的解决方案,那么我恐怕是这样的:浮点数很快,但是有很小的舍入误差——在显示结果时总是舍入到一些合理的值。


为了避免这种情况,您应该使用整数值而不是浮点。所以当你想让两个位置的精度值为*100时,对于三个位置,使用1000。显示时,使用格式化程序放入分隔符。

许多系统忽略了使用小数的方法。这就是为什么许多系统使用美分(整数)而不是美元/欧元(浮点)工作的原因。


问题

浮点不能精确存储所有的十进制值。因此,当使用浮点格式时,输入值总是会出现舍入错误。当然,输入上的错误会导致输出上的错误。在离散函数或运算符的情况下,函数或运算符离散的点周围的输出可能存在很大差异。

浮点值的输入和输出

所以,在使用浮点变量时,应该始终注意这一点。无论您希望从带有浮点的计算中得到什么样的输出,在显示之前都应该记住这一点。当只使用连续函数和运算符时,通常会舍入到所需的精度(不要截断)。用于将浮点数转换为字符串的标准格式设置功能通常会为您做到这一点。由于舍入会增加一个误差,导致总误差超过所需精度的一半,因此应根据输入的预期精度和输出的所需精度来校正输出。你应该

  • 将输入舍入到预期的精度,或确保不能以更高的精度输入任何值。
  • 在对输出进行舍入/格式化之前,向其添加一个小值,该值小于或等于所需精度的1/4,并且大于输入舍入错误和计算过程中造成的最大预期错误。如果这不可能,则所用数据类型的精度组合不足以为计算提供所需的输出精度。

这两件事通常都不做,在大多数情况下,不做造成的差异太小,对大多数用户来说都不重要,但是我已经有了一个项目,如果没有这些更正,输出就不能被用户接受。

离散函数或运算符(如模)

当涉及到离散的运算符或函数时,可能需要额外的修正来确保输出如预期的那样。舍入和在舍入前加上小的修正不能解决这个问题。可能需要在应用离散函数或运算符后立即对中间计算结果进行特殊检查/更正。对于特定情况(modula operator),请参阅我关于问题的答案:为什么modula operator在javascript中返回分数?

最好避免出现问题

通过使用数据类型(整数或定点格式)进行类似计算,通常可以更有效地避免这些问题,这样可以存储预期的输入,而不会出现舍入错误。一个例子是,您不应该在财务计算中使用浮点值。


如果您想绕过此问题进行小型操作,可以使用parseFloat()toFixed()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a = 0.1;
b = 0.2;

a + b = 0.30000000000000004;

c = parseFloat((a+b).toFixed(2));

c = 0.3;

a = 0.3;
b = 0.2;

a - b = 0.09999999999999998;

c = parseFloat((a-b).toFixed(2));

c = 0.1;

0.6*3太棒了!))对我来说这很好:

1
2
3
4
5
function dec( num )
{
    var p = 100;
    return Math.round( num * p ) / p;
}

非常简单)


看看定点算术。如果你想操作的数字范围很小(如货币),它可能会解决你的问题。我会把它四舍五入为几个小数值,这是最简单的解决方案。


试试我的chiliadic算术库,你可以在这里看到。如果你想要更高版本,我可以给你一个。


您不能用二进制浮点类型(这是EcmaScript用来表示浮点值的)精确表示大多数小数部分。因此,除非使用任意精度的算术类型或基于小数的浮点类型,否则没有一个优雅的解决方案。例如,随Windows一起提供的计算器应用程序现在使用任意精度算法来解决这个问题。


enter image description here

1
2
3
4
5
6
7
8
9
10
11
12
    You can use library https://github.com/MikeMcl/decimal.js/.
    it will   help  lot to give proper solution.
    javascript console output 95 *722228.630 /100 = 686117.1984999999
    decimal library implementation
    var firstNumber = new Decimal(95);
    var secondNumber = new Decimal(722228.630);
    var thirdNumber = new Decimal(100);
    var partialOutput = firstNumber.times(secondNumber);
    console.log(partialOutput);
    var output = new Decimal(partialOutput).div(thirdNumber);
    alert(output.valueOf());
    console.log(output.valueOf())== 686117.1985

我有一个讨厌的舍入误差问题与3型。有时当我应该得到0的时候,我会得到0.000…01。这很容易处理,只需测试<=0.01。但有时我会得到2.9999999998。哎哟!

BigNumbers解决了这个问题,但引入了另一个有点讽刺意味的问题。当试图将8.5加载到bignumbers中时,我被告知它实际上是8.4999…并且有超过15个有效数字。这意味着大数字无法接受(我认为我提到这个问题有点讽刺)。

反讽问题的简单解决方案:

1
2
3
4
x = Math.round(x*100);
// I only need 2 decimal places, if i needed 3 I would use 1,000, etc.
x = x / 100;
xB = new BigNumber(x);

注意,对于一般用途,这种行为可能是可以接受的。当比较这些浮点值以确定适当的操作时,就会出现问题。随着ES6的出现,一个新的常数Number.EPSILON被定义为确定可接受的误差范围:所以不要像这样进行比较

1
0.1 + 0.2 === 0.3 // which returns false

可以定义自定义比较函数,如下所示:

1
2
3
4
function epsEqu(x, y) {
    return Math.abs(x - y) < Number.EPSILON;
}
console.log(epsEqu(0.1+0.2, 0.3)); // true

资料来源:http://2ality.com/2015/04/numbers-math-es6.html numberepsilon


使用数字(1.23443).tofixed(2);它将打印1.23

1
2
3
4
5
function test(){
    var x = 0.1 * 0.2;
    document.write(Number(x).toFixed(2));
}
test();

使用

1
2
var x = 0.1*0.2;
 x =Math.round(x*Math.pow(10,2))/Math.pow(10,2);


你说得对,原因是浮点数的精度有限。将有理数存储为两个整数的除法,在大多数情况下,您可以在不损失任何精度的情况下存储数字。在打印时,您可能希望将结果显示为分数。对于我提出的表示,它变得微不足道。

当然,这对无理数没什么帮助。但是您可能希望优化您的计算,使它们产生最少的问题(例如,检测像sqrt(3)^2)这样的情况)。


不是很优雅,但做了这个工作(去掉尾随的零)

1
2
var num = 0.1*0.2;
alert(parseFloat(num.toFixed(10))); // shows 0.02


这对我很有用:

1
2
3
4
5
6
function round_up( value, precision ) {
    var pow = Math.pow ( 10, precision );
    return ( Math.ceil ( pow * value ) + Math.ceil ( pow * value - Math.ceil ( pow * value ) ) ) / pow;
}

round_up(341.536, 2); // 341.54


使用以下功能输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var toFixedCurrency = function(num){
    var num = (num).toString();
    var one = new RegExp(/\.\d{1}$/).test(num);
    var two = new RegExp(/\.\d{2,}/).test(num);
    var result = null;

    if(one){ result = num.replace(/\.(\d{1})$/, '.$10');
    } else if(two){ result = num.replace(/\.(\d{2})\d*/, '.$1');
    } else { result = num*100; }

    return result;
}

function test(){
    var x = 0.1 * 0.2;
    document.write(toFixedCurrency(x));
}

test();

注意输出toFixedCurrency(x)


当添加两个浮点值时,它永远不会给出精确的值,因此我们需要将其固定到某个有助于进行比较的数字。

console.log((parsefloat(0.1)+parsefloat(0.2)).tofixed(1)==parsefloat(0.3).tofixed(1));


我不太擅长编程,但我对这个主题很感兴趣,所以我试图理解如何在不使用任何库或脚本的情况下解决这个问题。

我在草稿纸上写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var toAlgebraic = function(f1, f2) {
    let f1_base = Math.pow(10, f1.split('.')[1].length);
    let f2_base = Math.pow(10, f2.split('.')[1].length);
    f1 = parseInt(f1.replace('.', ''));
    f2 = parseInt(f2.replace('.', ''));

    let dif, base;
    if (f1_base > f2_base) {
        dif = f1_base / f2_base;
        base = f1_base;
        f2 = f2 * dif;
    } else {
        dif = f2_base / f1_base;
        base = f2_base;
        f1 = f1 * dif;
    }

    return (f1 * f2) / base;
};

console.log(0.1 * 0.2);
console.log(toAlgebraic("0.1","0.2"));

您可能需要重构此代码,因为我不擅长编程:)


如果不想每次都调用函数,可以创建一个处理转换的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Decimal {
  constructor(value = 0, scale = 4) {
    this.intervalValue = value;
    this.scale = scale;
  }

  get value() {
    return this.intervalValue;
  }

  set value(value) {
    this.intervalValue = Decimal.toDecimal(value, this.scale);
  }

  static toDecimal(val, scale) {
    const factor = 10 ** scale;
    return Math.round(val * factor) / factor;
  }
}

用途:

1
2
3
4
5
6
const d = new Decimal(0, 4);
d.value = 0.1 + 0.2;              // 0.3
d.value = 0.3 - 0.2;              // 0.1
d.value = 0.1 + 0.2 - 0.3;        // 0
d.value = 5.551115123125783e-17;  // 0
d.value = 1 / 9;                  // 0.1111

当然,在处理小数时,需要注意:

1
2
3
4
d.value = 1/3 + 1/3 + 1/3;   // 1
d.value -= 1/3;              // 0.6667
d.value -= 1/3;              // 0.3334
d.value -= 1/3;              // 0.0001

理想情况下,您希望使用高比例(如12),然后在需要展示或存储时将其转换为低比例。就我个人而言,我曾尝试创建uint8array并尝试创建精度值(很像SQL decimal类型),但由于javascript不允许您重载运算符,因此它会变得有点单调,无法使用基本的数学运算符(+-/*,而是使用类似的函数add()substract()mult()。为了我的需要,这不值得。

但是如果您确实需要这样的精度,并且愿意忍受使用函数进行数学运算,那么我推荐使用decimal.js库。


我在这里有个工作区。例如,用10e^x乘以不适用于1.1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function sum(a,b){
    var tabA = (a +"").split(".");
    var tabB = (b +"").split(".");
    decA = tabA.length>1?tabA[1].length:0;
    decB = tabB.length>1?tabB[1].length:0;
    a = (tabA[0]+tabA[1])*1.0;
    b = (tabB[0]+tabB[1])*1.0;
    var diff = decA-decB;
    if(diff >0){
        //a has more decimals than b
        b=b*Math.pow(10,diff);
        return (a+b)/Math.pow(10,decA);
    }else if (diff<0){
        //a has more decimals than b
        a=a*Math.pow(10,-diff);
                return (a+b)/Math.pow(10,decB);
    }else{
        return (a+b)/Math.pow(10,decA);
    }      
}

吓人,但很管用:)


要处理任意浮点数:

1
2
3
4
5
6
7
8
function shorten(num) {
    num += 0.000000001;// to deal with"12.03999999997" form
    num += '';
    return num.replace(/(\.\d*?)0{5,}\d+$/, '$1') * 1;
}

console.log(1.2+1.9===1.3+1.8);// false
console.log(shorten(1.2+1.9)===shorten(1.3+1.8));// true

您可以使用regex来检查数字是否以0的长字符串结尾,后跟一个小的余数:

1
2
3
4
// using max number of 0s = 8, maximum remainder = 4 digits
x = 0.1048000000000051
parseFloat(x.toString().replace(/(\.[\d]+[1-9])0{8,}[1-9]{0,4}/, '$1'), 10)
// = 0.1048