Last active
November 29, 2019 09:55
-
-
Save xuanfeng/0f5c09ed0290290603e828fce170e211 to your computer and use it in GitHub Desktop.
浮点数运算
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* operation 浮点数运算 | |
* https://github.com/nefe/number-precision | |
* @desc 解决浮动运算问题,避免小数点后产生多位数和计算精度损失。 | |
* 问题示例:2.3 + 2.4 = 4.699999999999999,1.0 - 0.9 = 0.09999999999999998 | |
*/ | |
/** | |
* 精确加法 | |
*/ | |
function add(num1, num2) { | |
const others = [] | |
for (let _i = 2; _i < arguments.length; _i++) { | |
others[_i - 2] = arguments[_i] | |
} | |
if (others.length > 0) { | |
return add.apply(void 0, [add(num1, num2), others[0]].concat(others.slice(1))) | |
} | |
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2))) | |
return (multiply(num1, baseNum) + multiply(num2, baseNum)) / baseNum | |
} | |
/** | |
* 精确减法 | |
*/ | |
function subtract(num1, num2) { | |
const others = [] | |
for (let _i = 2; _i < arguments.length; _i++) { | |
others[_i - 2] = arguments[_i] | |
} | |
if (others.length > 0) { | |
return subtract.apply(void 0, [subtract(num1, num2), others[0]].concat(others.slice(1))) | |
} | |
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2))) | |
return (multiply(num1, baseNum) - multiply(num2, baseNum)) / baseNum | |
} | |
/** | |
* 精确乘法 | |
*/ | |
function multiply(num1, num2) { | |
const others = [] | |
for (let _i = 2; _i < arguments.length; _i++) { | |
others[_i - 2] = arguments[_i] | |
} | |
if (others.length > 0) { | |
return multiply.apply(void 0, [multiply(num1, num2), others[0]].concat(others.slice(1))) | |
} | |
const num1Changed = float2Fixed(num1) | |
const num2Changed = float2Fixed(num2) | |
const baseNum = digitLength(num1) + digitLength(num2) | |
const leftValue = num1Changed * num2Changed | |
checkBoundary(leftValue) | |
return leftValue / Math.pow(10, baseNum) | |
} | |
/** | |
* 精确除法 | |
*/ | |
function divide(num1, num2) { | |
const others = [] | |
for (let _i = 2; _i < arguments.length; _i++) { | |
others[_i - 2] = arguments[_i] | |
} | |
if (others.length > 0) { | |
return divide.apply(void 0, [divide(num1, num2), others[0]].concat(others.slice(1))) | |
} | |
const num1Changed = float2Fixed(num1) | |
const num2Changed = float2Fixed(num2) | |
checkBoundary(num1Changed) | |
checkBoundary(num2Changed) | |
// fix: 类似 10 ** -4 为 0.00009999999999999999,strip 修正 | |
return multiply((num1Changed / num2Changed), strip(Math.pow(10, digitLength(num2) - digitLength(num1)))) | |
} | |
/** | |
* 把错误的数据转正 | |
* strip(0.09999999999999998)=0.1 | |
*/ | |
function strip(num, precision) { | |
if (!num) return num | |
if (precision === void 0) { precision = 12 } | |
return +parseFloat(num.toPrecision(precision)) | |
} | |
/** | |
* Return digits length of a number | |
* @param {*number} num Input number | |
*/ | |
function digitLength(num) { | |
if (!num) return num | |
// Get digit length of e | |
const eSplit = num.toString().split(/[eE]/) | |
const len = (eSplit[0].split('.')[1] || '').length - (+(eSplit[1] || 0)) | |
return len > 0 ? len : 0 | |
} | |
/** | |
* 把小数转成整数,支持科学计数法。如果是小数则放大成整数 | |
* @param {*number} num 输入数 | |
*/ | |
function float2Fixed(num) { | |
if (!num) return num | |
if (num.toString().indexOf('e') === -1) { | |
return Number(num.toString().replace('.', '')) | |
} | |
const dLen = digitLength(num) | |
return dLen > 0 ? strip(num * Math.pow(10, dLen)) : num | |
} | |
/** | |
* 检测数字是否越界,如果越界给出提示 | |
* @param {*number} num 输入数 | |
*/ | |
function checkBoundary(num) { | |
if (!num) return num | |
if (_boundaryCheckingState) { | |
if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { | |
global.console.warn(num + " is beyond boundary when transfer to integer, the results may not be accurate") | |
} | |
} | |
} | |
/** | |
* 四舍五入 | |
*/ | |
function round(num, ratio) { | |
const base = Math.pow(10, ratio) | |
return divide(Math.round(multiply(num, base)), base) | |
} | |
/** | |
* 是否进行边界检查,默认开启 | |
* @param flag 标记开关,true 为开启,false 为关闭,默认为 true | |
*/ | |
let _boundaryCheckingState = true | |
function enableBoundaryChecking(flag) { | |
if (flag === void 0) { flag = true } | |
_boundaryCheckingState = flag | |
} | |
export default { | |
add, | |
subtract, | |
multiply, | |
divide, | |
strip, | |
round, | |
digitLength, | |
float2Fixed, | |
enableBoundaryChecking, | |
} | |
// case | |
console.log(operation.add(0.1, 0.2), 0.1 + 0.2) // 0.3 0.30000000000000004 | |
console.log(operation.subtract(300.73, 70), 300.73 - 70) // 230.73 230.73000000000002 | |
console.log(operation.subtract(2, 0.14), 2 - 0.14) // 1.86 1.8599999999999999 | |
console.log(operation.subtract(1.05, 1), 1.05 - 1) // 0.05 0.050000000000000044 | |
console.log(operation.subtract(300.73, 300), 300.73 - 300) // 0.73 0.7300000000000182 | |
console.log(operation.multiply(1073.4, 100), 1073.4 * 100) // 107340 107340.00000000001 | |
console.log(operation.multiply(7.11, 10), 7.11 * 10) // 71.1 71.10000000000001 | |
console.log(operation.divide(7.11, 10), 7.11 / 10) // 0.711 0.7110000000000001 | |
// 以下乘除不准确 | |
/* operation 浮点数运算 | |
* add / subtract / multiply /divide | |
* operation.add(0.1, 0.2) >> 0.3 | |
* operation.multiply(19.9, 100) >> 1990 | |
* | |
*/ | |
import _ from 'lodash' | |
/* | |
* 判断obj是否为一个整数 | |
*/ | |
function isInteger(obj) { | |
return _.isInteger(_.toNumber(obj)) | |
} | |
/* | |
* 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100 | |
* @param floatNum {number} 小数 | |
* @return {object} | |
* {times:100, num: 314} | |
*/ | |
function toInteger(floatNum) { | |
if (!floatNum) { | |
return { | |
times: 1, | |
num: floatNum, | |
} | |
} | |
const ret = { times: 1, num: 0 } | |
if (isInteger(floatNum)) { | |
ret.num = floatNum | |
return ret | |
} | |
const strfi = floatNum + '' | |
const dotPos = strfi.indexOf('.') | |
const len = strfi.substr(dotPos + 1).length | |
const times = Math.pow(10, len) | |
const intNum = Number(floatNum.toString().replace('.', '')) | |
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) { | |
const o1 = toInteger(a) | |
const o2 = toInteger(b) | |
const n1 = o1.num | |
const n2 = o2.num | |
const t1 = o1.times | |
const t2 = o2.times | |
const max = t1 > t2 ? t1 : t2 | |
let 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': | |
result = (n1 / n2) * t2 / t1 | |
return result | |
} | |
} | |
export default { | |
add() { | |
return operation(...arguments, 'add') | |
}, | |
subtract() { | |
return operation(...arguments, 'subtract') | |
}, | |
multiply() { | |
return operation(...arguments, 'multiply') | |
}, | |
divide() { | |
return operation(...arguments, 'divide') | |
}, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment