通过后面的几个小章节,将会大致为大家介绍以下几个方面内容:
● 浮点数的二进制表示方式
● IEEE 754 标准是什么
● 避开浮点数计算精度问题的方案
● 测试框架(Mocha)的基本用法
一、计算机的运算方式
㈠ 如何将小数转成二进制
㈡ IEEE 754 标准
尾数部分M通常都是规格化表示的,即非"0"的尾数其第一位总是"1",而这一位也称隐藏位,因为存储时候这一位是会被省略的。比如保存1.0011时,只保存0011,等读取的时候才把第一位的1加上去,这样做相当于多保存了1位有效数字。
㈢ 浮点数运算
最后0.1 => 0,01111111100;1100110011001100110011001100110011001100110011001101(0)
注:要注意0舍1入的原则。之所以右移一位,尾数补的是1,是因为隐藏位的数值为1(默认是不存储的,只有读取的时候才加上)③ 规格化
针对步骤②的结果,需要 右规(即尾数右移1位,阶码加1)注:右规操作,可能会导致低位丢失,引起误差,造成精度问题。所以就需要步骤④的舍入操作
二、浮点数精度问题的解决方法
㈠ 简单解决方案
'use strict'
var accAdd = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var dec1, dec2, times;
try { dec1 = countDecimals(num1)+1; } catch (e) { dec1 = 0; }
try { dec2 = countDecimals(num2)+1; } catch (e) { dec2 = 0; }
times = Math.pow(10, Math.max(dec1, dec2));
// var result = (num1 * times + num2 * times) / times;
var result = (accMul(num1, times) + accMul(num2, times)) / times;
return getCorrectResult("add", num1, num2, result);
// return result;
};
var accSub = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var dec1, dec2, times;
try { dec1 = countDecimals(num1)+1; } catch (e) { dec1 = 0; }
try { dec2 = countDecimals(num2)+1; } catch (e) { dec2 = 0; }
times = Math.pow(10, Math.max(dec1, dec2));
// var result = Number(((num1 * times - num2 * times) / times);
var result = Number((accMul(num1, times) - accMul(num2, times)) / times);
return getCorrectResult("sub", num1, num2, result);
// return result;
};
var accDiv = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var t1 = 0, t2 = 0, dec1, dec2;
try { t1 = countDecimals(num1); } catch (e) { }
try { t2 = countDecimals(num2); } catch (e) { }
dec1 = convertToInt(num1);
dec2 = convertToInt(num2);
var result = accMul((dec1 / dec2), Math.pow(10, t2 - t1));
return getCorrectResult("div", num1, num2, result);
// return result;
};
var accMul = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var times = 0, s1 = num1.toString(), s2 = num2.toString();
try { times += countDecimals(s1); } catch (e) { }
try { times += countDecimals(s2); } catch (e) { }
var result = convertToInt(s1) * convertToInt(s2) / Math.pow(10, times);
return getCorrectResult("mul", num1, num2, result);
// return result;
};
var countDecimals = function(num) {
var len = 0;
try {
num = Number(num);
var str = num.toString().toUpperCase();
if (str.split('E').length === 2) { // scientific notation
var isDecimal = false;
if (str.split('.').length === 2) {
str = str.split('.')[1];
if (parseInt(str.split('E')[0]) !== 0) {
isDecimal = true;
}
}
let x = str.split('E');
if (isDecimal) {
len = x[0].length;
}
len -= parseInt(x[1]);
} else if (str.split('.').length === 2) { // decimal
if (parseInt(str.split('.')[1]) !== 0) {
len = str.split('.')[1].length;
}
}
} catch(e) {
throw e;
} finally {
if (isNaN(len) || len < 0) {
len = 0;
}
return len;
}
};
var convertToInt = function(num) {
num = Number(num);
var newNum = num;
var times = countDecimals(num);
var temp_num = num.toString().toUpperCase();
if (temp_num.split('E').length === 2) {
newNum = Math.round(num * Math.pow(10, times));
} else {
newNum = Number(temp_num.replace(".", ""));
}
return newNum;
};
var getCorrectResult = function(type, num1, num2, result) {
var temp_result = 0;
switch (type) {
case "add":
temp_result = num1 + num2;
break;
case "sub":
temp_result = num1 - num2;
break;
case "div":
temp_result = num1 / num2;
break;
case "mul":
temp_result = num1 * num2;
break;
}
if (Math.abs(result - temp_result) > 1) {
return temp_result;
}
return result;
};
基本用法:
加法: accAdd(0.1, 0.2) // 得到结果:0.3
减法: accSub(1, 0.9) // 得到结果:0.1
除法: accDiv(2.2, 100) // 得到结果:0.022
乘法: accMul(7, 0.8) // 得到结果:5.6
countDecimals()方法:计算小数位的长度
convertToInt()方法:将小数转成整数
getCorrectResult()方法:确认我们的计算结果无误,以防万一
㈡ 使用特殊进制类型类库
测试可以保证我们的代码质量。而我们的那个精确计算的方案需要大量的测试来验证其正确性,通过编写测试代码进行测试省时省力,而且方便以后修改代码,能很快确认代码是否有误。
测试使用的是现在较流行的JavaScript测试框架Mocha,在浏览器和Node环境都可以使用。npm install mocha chai --save
④ 测试代码的编写
if (typeof module !== 'undefined' && module.exports) {
var calc = {};
calc.countDecimals = countDecimals;
calc.convertToInt = convertToInt;
calc.getCorrectResult = getCorrectResult;
calc.accAdd = accAdd;
calc.accSub = accSub;
calc.accDiv = accDiv;
calc.accMul = accMul;
module.exports = calc;
}
④-2 编写测试代码(以测试countDecimals方法为例)
var chai = require('chai');
var assert = chai.assert; // Using Assert style
var expect = chai.expect; // Using Expect style
var should = chai.should(); // Using Should style
var calc = require('./accurateCalculate');
var countDecimals = calc.countDecimals;
function test_countDecimals(info, num, expected) {
it(info, function(done) {
expect(countDecimals(num)).to.be.equal(expected);
done();
});
}
describe('TEST countDecimals', function() {
describe('TEST Number', function() {
test_countDecimals('3', 3, 0);
test_countDecimals('3.00', 3.00, 0);
test_countDecimals('3.01', 3.01, 2);
test_countDecimals('3e0', 3e0, 0);
test_countDecimals('3e10', 3e10, 0);
test_countDecimals('3e-10', 3e-10, 10);
test_countDecimals('3.01e0', 3.01e0, 2);
test_countDecimals('3.01e10', 3.01e10, 0);
test_countDecimals('3.01e-10', 3.01e-10, 12);
});
describe('TEST String', function() {
test_countDecimals('3', '3', 0);
test_countDecimals('3.00', '3.00', 0);
test_countDecimals('3.01', '3.01', 2);
test_countDecimals('3.00e0', '3.00e0', 0);
test_countDecimals('3.00e10', '3.00e10', 0);
test_countDecimals('3.00e-10', '3.00e-10', 10);
test_countDecimals('30e-1', '30e-1', 0);
test_countDecimals('30e-2', '30e-2', 1);
});
});
首先需要
require语句,导入相关类库以及刚才待测js中export出来的对象。
expect(调用待测函数()).to.be.equal(期待的返回值);
所谓"断言",就是判断被测函数的实际执行结果与预期结果是否一致。
"scripts": {
"test": "mocha test.js"
},
⑥ 运行测试代码
测试框架 Mocha 实例教程:
http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html
JS浮点数计算精度问题是因为某些小数没法用二进制精确表示出来。JS使用的是IEEE 754双精度浮点规则。
而规避浮点数计算精度问题,可通过以下几种方法:
● 调用round() 方法四舍五入或者toFixed() 方法保留指定的位数(对精度要求不高,可用这种方法)
● 将小数转为整数再做计算,即前文提到的那个简单的解决方案
● 使用特殊的进制数据类型,如前文提到的bignumber(对精度要求很高,可借助这些相关的类库)