转贴:http://rockyee.iteye.com/blog/891538
摘要:
由于计算机是用二进制来存储和处理数字,不能精确表示浮点数,而JavaScript中没有相应的封装类来处理浮点数运算,直接计算会导致运算精度丢失。
为了避免产生精度差异,把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完毕再降级(除以10的n次幂),这是大部分编程语言处理精度差异的通用方法。
关键词:
计算精度 四舍五入 四则运算 精度丢失
1. 疑惑
我们知道,几乎每种编程语言都提供了适合货币计算的类。例如C#提供了decimal,Java提供了BigDecimal,JavaScript提供了Number……
由于之前用decimal和BigDecimal用得很好,没有产生过精度问题,所以一直没有怀疑过JavaScript的Number类型,以为可以直接使用Number类型进行计算。但是直接使用是有问题的。
我们先看看四舍五入的如下代码:
- alert(Number(0.009).toFixed(2));
- alert(Number(162.295).toFixed(2));
按正常结果,应该分别弹出0.01和162.30。但实际测试结果却是在不同浏览器中得到的是不同的结果:
在ie6、7、8下得到0.00和162.30,第一个数截取不正确;
在firefox中得到0.01和162.29,第二个数截取不正确;
在opera下得到0.01和162.29,第二个数截取不正确
我们再来看看四则运算的代码:
- alert(1/3);
-
alert(0.1 + 0.2);
-
alert(-0.09 - 0.01);
-
alert(0.012345 * 0.000001);
-
alert(0.000001 / 0.0001);
按正常结果,除第一行外(因为其本身就不能除尽),其他都应该要得到精确的结果,从弹出的结果我们却发现不是我们想要的正确结果。是因为没有转换成Number类型吗?我们转换成Number后再计算看看:
- alert(Number(1)/Number(3));
-
alert(Number(0.1) + Number(0.2));
-
alert(Number(-0.09) – Number(0.01));
-
alert(Number(0.012345) * Number(0.000001));
-
alert(Number(0.000001) / Number(0.0001));
还是一样的结果,看来javascript默认把数字识别为number类型。为了验证这一点,我们用typeof弹出类型看看:
- alert(typeof(1));
-
alert(typeof(1/3));
-
alert(typeof(-0.09999999));
2. 原因
为什么会产生这种精度丢失的问题呢?是javascript语言的bug吗?
我们回忆一下大学时学过的计算机原理,计算机执行的是二进制算术,当十进制数不能准确转换为二进制数时,这种精度误差就在所难免。
再查查javascript的相关资料,我们知道javascript中的数字都是用浮点数表示的,并规定使用IEEE 754 标准的双精度浮点数表示:
IEEE 754 规定了两种基本浮点格式:单精度和双精度。
IEEE单精度格式具有24 位有效数字精度(包含符号号),并总共占用32 位。
IEEE双精度格式具有53 位有效数字精度(包含符号号),并总共占用64 位。
这种结构是一种科学表示法,用符号(正或负)、指数和尾数来表示,底数被确定为2,也就是说是把一个浮点数表示为尾数乘以2的指数次方再加上符号。下面来看一下具体的规格:
|
符号位 |
指数位 |
小数部分 |
指数偏移量 |
单精度浮点数 |
1位(31) |
8位(30-23) |
23位(22-00) |
127 |
双精度浮点数 |
1位(63) |
11位(62-52) |
52位(51-00) |
1023 |
我们以单精度浮点数来说明:
指数是8位,可表达的范围是0到255
而对应的实际的指数是-127到+128
这里特殊说明,-127和+128这两个数据在IEEE当中是保留的用作多种用途的
-127表示的数字是0
128和其他位数组合表示多种意义,最典型的就是NAN状态。
知道了这些,我们来模拟计算机的进制转换的计算,就找一个简单的0.1+0.2来推演吧(引用自http://blog.csdn.net/xujiaxuliang/archive/2010/10/13/5939573.aspx):
- 十进制0.1
-
=> 二进制0.00011001100110011…(循环0011)
-
=>尾数为1.1001100110011001100…1100(共52位,除了小数点左边的1),指数为-4(二进制移码为00000000010),符号位为0
-
=> 计算机存储为:0 00000000100 10011001100110011…11001
-
=> 因为尾数最多52位,所以实际存储的值为0.00011001100110011001100110011001100110011001100110011001
-
而十进制0.2
-
=> 二进制0.0011001100110011…(循环0011)
-
=>尾数为1.1001100110011001100…1100(共52位,除了小数点左边的1),指数为-3(二进制移码为00000000011),符号位为0
-
=> 存储为:0 00000000011 10011001100110011…11001
-
因为尾数最多52位,所以实际存储的值为0.00110011001100110011001100110011001100110011001100110011
- 那么两者相加得:
-
0.00011001100110011001100110011001100110011001100110011001
-
+ 0.00110011001100110011001100110011001100110011001100110011
-
= 0.01001100110011001100110011001100110011001100110011001100
-
转换成10进制之后得到:0.30000000000000004
从上述的推演过程我们知道,这种误差是难免的,c#的decimal和Java的BigDecimal之所以没有出现精度差异,只是因为在其内部作了相应处理,把这种精度差异给屏蔽掉了,而javascript是一种弱类型的脚本语言,本身并没有对计算精度做相应的处理,这就需要我们另外想办法处理了。
3. 解决办法
3.1 升级降级
从上文我们已经知道,javascript中产生精度差异的原因是计算机无法精确表示浮点数,连自身都不能精确,运算起来就更加得不到精确的结果了。那么怎么让计算机精确认识要计算的数呢?
我们知道十进制的整数和二进制是可以互相进行精确转换的,那么我们把浮点数升级(乘以10的n次幂)成计算机能够精确识别的整数来计算,计算完毕之后再降级(除以10的n次幂),不就得到精确的结果了吗?好,就这么办!
我们知道,Math.pow(10,scale)可以得到10的scale次方,那么就把浮点数直接乘以Math.pow(10,scale)就可以了吗?我最初就是这么想的,但后来却发现一些数字运算后实际结果与我们的猜想并不一致。我们来看看这个简单的运算:
按常理应该返回51206,但实际结果却是51205.99999999999。奇怪吧?其实也不奇怪,这是因为浮点数不能精确参与乘法运算,即使这个运算很特殊(只是乘以10的scale次方进行升级)。如此我们就不能直接乘以10的scale次方进行升级,那就让我们自己来挪动小数点吧。
怎么挪动小数点肯定大家是各有妙招,此处附上我写的几个方法:
-
-
-
-
-
-
-
-
-
-
String.prototype.padLeft = function(nSize, ch)
- {
-
var len = 0;
-
var s = this ? this : "";
-
ch = ch ? ch : '0';
-
- len = s.length;
-
while (len < nSize)
- {
- s = ch + s;
- len++;
- }
-
return s;
- }
-
-
-
-
-
-
-
-
-
-
-
String.prototype.padRight = function(nSize, ch)
- {
-
var len = 0;
-
var s = this ? this : "";
-
ch = ch ? ch : '0';
-
- len = s.length;
-
while (len < nSize)
- {
- s = s + ch;
- len++;
- }
-
return s;
- }
-
-
-
-
-
-
-
-
String.prototype.movePointLeft = function(scale)
- {
-
var s, s1, s2, ch, ps, sign;
-
ch = '.';
-
sign = '';
-
s = this ? this : "";
-
-
if (scale <= 0) return s;
-
ps = s.split('.');
-
s1 = ps[0] ? ps[0] : "";
-
s2 = ps[1] ? ps[1] : "";
-
if (s1.slice(0, 1) == '-')
- {
- s1 = s1.slice(1);
-
sign = '-';
- }
-
if (s1.length <= scale)
- {
-
ch = "0.";
- s1 = s1.padLeft(scale);
- }
-
return sign + s1.slice(0, -scale) + ch + s1.slice(-scale) + s2;
- }
-
-
-
-
-
-
-
-
String.prototype.movePointRight = function(scale)
- {
-
var s, s1, s2, ch, ps;
-
ch = '.';
-
s = this ? this : "";
-
-
if (scale <= 0) return s;
-
ps = s.split('.');
-
s1 = ps[0] ? ps[0] : "";
-
s2 = ps[1] ? ps[1] : "";
-
if (s2.length <= scale)
- {
-
ch = '';
- s2 = s2.padRight(scale);
- }
-
return s1 + s2.slice(0, scale) + ch + s2.slice(scale, s2.length);
- }
-
-
-
-
-
-
-
-
String.prototype.movePoint = function(scale)
- {
-
if (scale >= 0)
-
return this.movePointRight(scale);
-
else
-
return this.movePointLeft(-scale);
- }
这样我们升级降级都可以转换成字符串后调用String对象的自定义方法movePoint了,乘以10的scale次方我们传正整数scale,除以10的scale次方我们传负整数-scale。
再来看看我们之前升级512.06的代码,采用自定义方法的调用代码变成这样:
- alert(512.06.toString().movePoint(2));
这样直接挪动小数点就不怕它不听话出现一长串数字了(*^__^*)。 当然,movePoint方法得到的结果是字符串,如果要转成Number类型也很方便(怎么转就不再废话了)。
3.2 四舍五入
好,有了升级降级的基础,我们来看看四舍五入的方法,由于不同浏览器对Number的toFixed方法有不同的支持,我们需要用自己的方法去覆盖浏览器的默认实现。
有一个简单的办法是我们自己来判断要截取数据的后一位是否大于等于5,然后进行舍或者入。我们知道Math.ceil方法是取大于等于指定数的最小整数,Math.floor方法是取小于等于指定数的最大整数,于是我们可以利用这两个方法来进行舍入处理,先将要进行舍入的数升级要舍入的位数 scale(乘以10的scale次方),进行ceil或floor取整后,再降级要舍入的位数scale(除以10的scale次方)。
代码如下:
- Number.prototype.toFixed = function(scale)
- {
-
var s, s1, s2, start;
-
-
s1 = this + "";
-
start = s1.indexOf(".");
- s = s1.movePoint(scale);
-
-
if (start >= 0)
- {
- s2 = Number(s1.substr(start + scale + 1, 1));
-
if (s2 >= 5 && this >= 0 || s2 < 5 && this < 0)
- {
- s = Math.ceil(s);
- }
-
else
- {
- s = Math.floor(s);
- }
- }
-
-
return s.toString().movePoint(-scale);
- }
覆盖Number类型的toFixed方法后,我们再来执行以下方法
- alert(Number(0.009).toFixed(2));
-
alert(Number(162.295).toFixed(2));
在ie6、7、8、firefox、Opera下分别进行验证,都能得到相应的正确的结果。
另一种方式是在网上找到的采用正则表达式来进行四舍五入,代码如下:
- Number.prototype.toFixed = function(scale)
- {
-
var s = this + "";
-
if (!scale) scale = 0;
-
if (s.indexOf(".") == -1) s += ".";
-
s += new Array(scale + 1).join("0");
-
if (new RegExp("^(-|\\+)?(\\d+(\\.\\d{0," + (scale + 1) + "})?)\\d*$").test(s))
- {
-
var s = "0" + RegExp.$2, pm = RegExp.$1, a = RegExp.$3.length, b = true;
-
if (a == scale + 2)
- {
- a = s.match(/\d/g);
-
if (parseInt(a[a.length - 1]) > 4)
- {
-
for (var i = a.length - 2; i >= 0; i--)
- {
- a[i] = parseInt(a[i]) + 1;
-
if (a[i] == 10)
- {
- a[i] = 0;
- b = i != 1;
- }
-
else
-
break;
- }
- }
-
s = a.join("").replace(new RegExp("(\\d+)(\\d{" + scale + "})\\d$"), "$1.$2");
- }
-
if (b) s = s.substr(1);
-
return (pm + s).replace(/\.$/, "");
- }
-
return this + "";
- }
经验证,这两个方法都能够进行准确的四舍五入,那么采用哪个方法好呢?实践出真知,我们写一个简单的方法来验证一下两种方式的性能:
- function testRound()
- {
-
var dt, dtBegin, dtEnd, i;
-
dtBegin = new Date();
-
for (i=0; i<100000; i++)
- {
-
dt = new Date();
-
Number("0." + dt.getMilliseconds()).toFixed(2);
- }
-
dtEnd = new Date();
- alert(dtEnd.getTime()-dtBegin.getTime());
- }
为了避免对同一个数字进行四舍五入运算有缓存问题,我们取当前毫秒数进行四舍五入。经验证,在同一台机器上运算10万次的情况下,用movePoint方法,平均耗时2500毫秒;用正则表达式方法,平均耗时4000毫秒。
分享到:
相关推荐
代码如下: /** * 加法运算,避免数据相加小数点后产生多位数和计算精度损失。 * * @param num1加数1 | num2加数2 */ function numAdd(num1, num2) { var baseNum, baseNum1, baseNum2; try { baseNum
JS 数字精度丢失的一些典型问题 JS 数字精度丢失的原因 解决方案(一个对象+一个函数) 一、JS数字精度丢失的一些典型问题 1. 两个简单的浮点数相加 0.1 + 0.2 != 0.3 // true 这真不是 Firebug 的问题,可以用...
由于JavaScript内部使用的是IEEE 754标准来存储和处理浮点数,这可能导致在进行小数运算时出现精度丢失问题,尤其是在涉及大数字或者循环计算时。本篇将深入探讨这个问题,并提供一些解决方案。 首先,我们来理解...
JavaScript中的数字类型主要基于IEEE 754标准的双精度64位浮点数格式进行存储,这在处理小数和大整数时会导致精度丢失问题。本文将深入探讨这两个问题及其背后的原理。 首先,最大整数问题。JavaScript能够表示的...
在JavaScript开发中,处理小数加减乘除运算时经常会遇到精度丢失的问题。由于JavaScript中的数字是以IEEE 754标准的双精度64位浮点数来表示的,这就意味着它可以精确地表示的整数范围大约在-***到***之间。当超出这...
标题所提到的知识点是关于在JavaScript编程语言中,如何处理和避免在进行数字计算时出现的精度误差问题。这个问题是数字在以浮点数形式存储时,因为计算机的存储架构是以二进制为基础的,导致一些十进制小数在二进制...
在JavaScript中处理浮点数时,经常会遇到小数精度丢失的问题。这是由于JavaScript内部使用的是64位浮点数格式(即IEEE 754标准)来表示数值,而这种格式在处理非二进制可精确表示的十进制小数时会产生误差。例如,...
使用示例: math.config({ number:'BigNumber' }) let result=math.parser().eval(a+ "-" + b); //a,b是需要计算的值,中间是运算符
在使用JavaScript进行小数运算时,由于JavaScript的内部运算规则是...希望通过本文的介绍,读者能够理解和掌握JavaScript处理小数精度丢失的技巧,并能够在实际编程工作中避免类似的问题,提升程序的准确性和可靠性。
在JavaScript编程语言中,精确计算是一项重要的需求,特别是在金融、科学计算或任何需要...通过深入理解和运用这些知识点,开发者可以有效地在JavaScript环境中实现高精度的数学计算,避免因浮点数不精确而导致的问题。
这种精度丢失的现象对于需要高精度计算的应用来说是一个常见的问题。本文将详细介绍Java中的 `double` 类型精度问题,并提供几种解决方法。 #### 一、Java Double 精度丢失的原因 1. **二进制表示限制**:`double`...
在JavaScript中处理小数运算时,经常会遇到一个令人头疼的问题——精度丢失。这主要是因为JavaScript中的数字采用IEEE 754标准下的双精度64位浮点数格式表示,而这种表示方法对于某些特定的十进制小数无法做到精确...
然而,对于更大但仍然可以表示的数字,如果涉及到精确计算,可能会遇到问题,因为超过了53位有效二进制数的限制,会导致精度丢失。为了定义一个可安全进行算术运算的数字范围,JavaScript提供了`Number.MAX_SAFE_...
然而,由于双精度浮点数的结构限制,JavaScript中的小数(尤其是不能用有限二进制精确表示的小数,如0.1和0.2)和大整数(超过52位的二进制数)常常面临精度丢失的问题。 对于小数的精度丢失问题,由于尾数位数有限...
JavaScript中的Number类型在处理大整数时存在精度丢失的问题,特别是在涉及19位或更多位数的整数时。这是由于JavaScript的浮点数表示方式(IEEE 754双精度浮点数)所限制的。JavaScript能精确表示的最大整数是2的53...
其实在我们使用正常的 + - * / 运算时,在某些情况下就会出现精度丢失的问题,如金额过大时,单纯的加减都有可能出现精度丢失,对于银行项目来说,是很敏感的,需要对每个加减乘除都进行一次方法处理,为此我整理...
为了解决上述提到的精度问题,可以通过自定义函数的方式来确保计算过程中的精度不会丢失。下面将详细介绍几种常用的方法,并提供具体的代码实现。 ##### 1. accDiv(arg1, arg2) —— 浮点数除法 此函数用于处理两...
JavaScript中出现计算精度问题的根本原因在于其内部使用了IEEE 754标准下的64位双精度浮点数来表示数值。这种表示方式允许使用非常广泛的数值范围,但是它无法准确地表示所有的小数。特别是对于那些不能被精确表示为...