原理
小数运算在各平台可能存在差异或不精确的情况,而整数运算没有这个问题,所以只要将所有小数全都转成整数就OK了。
源码
/**
* @author zp
*/
/**
* 多平台一致精确计算库,比decimal更轻量更快
* [Math.round、Math.min、Math.max,Math.floor、Math.ceil,这些系统方法一般情况下是可以放心使用的]
*/
const exactMath = Object.create(null);
module.exports = exactMath;
// 计算精度
// sin、cos、tan方法的误差小数点后16位
const ACCURACY_SIN_ERROR = 1e-16;
const ACCURACY_COS_ERROR = 1e-16;
const ACCURACY_TAN_ERROR = 1e-16;
// 角度弧度常量
const DEG = 57.29577951308232;
const RAD = 0.017453292519943295;
// 系统常量
exactMath.PI = 3.141592653589793;
exactMath.E = 2.718281828459045;
exactMath.LN2 = 0.6931471805599453;
exactMath.LN10 = 2.302585092994046;
exactMath.LOG2E = 1.4426950408889634;
exactMath.LOG10E = 0.4342944819032518;
exactMath.SQRT1_2 = 0.7071067811865476;
exactMath.SQRT2 = 1.4142135623730951;
/**
* 链式调用
* @example
* const value = exactMath.value(10).add(20.123).mul(2).sqrt().value;
*/
let chain = null;
exactMath.value = function (value) {
if (!chain) {
chain = {
value: 0,
valueOf() { return this.value; },
toString() { return String(this.value); }
}
for (const key in exactMath) {
if (key !== 'value' && typeof exactMath[key] === 'function') {
chain[key] = function (...args) {
this.value = exactMath[key].call(exactMath, this.value, ...args);
return this;
}
}
}
}
chain.value = value;
return chain;
}
/****************************************************基础****************************************************/
/**
* 获得小数位数
* @param {Number} num 浮点数
* @returns {Number}
*/
exactMath.getDecimalPlace = function (num) {
if (num && num !== Math.floor(num)) {
for (let n = 1, m = 10, temp = 0; n < 20; n += 1, m *= 10) {
temp = num * m;
if (temp == Math.floor(temp)) return n;
}
return 20;
} else {
return 0;
}
}
/**
* 保留n为小数,并四舍五入
* @example
* (2.335).toFixed(2)
* exactMath.toFixed(2.335, 2)
* @param {Number} num 浮点数
* @param {Number} n 整数
* @returns {Number}
*/
exactMath.toFixed = function (num, n = 0) {
if (n == 0) {
return Math.round(num);
} else {
const m = Math.pow(10, n);
return Math.round(num * (m * 10) / 10) / m;
}
}
exactMath.abs = function (x) {
return Math.abs(x);
}
exactMath.round = function (x) {
return Math.round(x);
}
exactMath.ceil = function (x) {
return Math.ceil(x)
}
exactMath.floor = function (x) {
return Math.floor(x)
}
exactMath.min = function (...args) {
return Math.min(...args);
}
exactMath.max = function (...args) {
return Math.max(...args);
}
/**
* 小数相加
* @param {Number} num1 浮点数
* @param {Number} num2 浮点数
* @returns {Number}
*/
exactMath.add = function (...args) {
if (args.length === 2) {
const num1 = args[0];
const num2 = args[1];
const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));
return (this.toFixed(num1 * m) + this.toFixed(num2 * m)) / m;
} else {
return args.reduce((a, b) => this.add(a, b))
}
};
/**
* 小数相减
* @param {Number} num1 浮点数
* @param {Number} num2 浮点数
* @returns {Number}
*/
exactMath.sub = function (...args) {
if (args.length === 2) {
const num1 = args[0];
const num2 = args[1];
const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));
return (this.toFixed(num1 * m) - this.toFixed(num2 * m)) / m;
} else {
return args.reduce((a, b) => this.sub(a, b))
}
};
/**
* 小数相乘
* @param {Number} num1 浮点数
* @param {Number} num2 浮点数
* @returns {Number}
*/
exactMath.mul = function (...args) {
if (args.length === 2) {
let num1 = args[0];
let num2 = args[1];
// 方案1:
// 直接相乘,但是相乘两数小数点过多会导致中间值[(n1 * m1) * (n2 * m2)]过大
// const n1 = this.getDecimalPlace(num1);
// const n2 = this.getDecimalPlace(num2);
// const m1 = Math.pow(10, n1);
// const m2 = Math.pow(10, n2);
// return (n1 * m1) * (n2 * m2) / (m1 * m2);
// 方案2:
// 用除法实现乘法,不会存在过大中间值
let n1 = this.getDecimalPlace(num1);
let n2 = this.getDecimalPlace(num2);
let m = Math.pow(10, n2);
num2 = m / this.toFixed(num2 * m);
m = Math.pow(10, Math.max(n1, this.getDecimalPlace(num2)));
m = this.toFixed(num1 * m) / this.toFixed(num2 * m);
let n = Math.min(this.getDecimalPlace(m), n1 + n2);
return this.toFixed(m, n);
} else {
return args.reduce((a, b) => this.mul(a, b))
}
};
/**
* 小数相除法
* @param {Number} num1 浮点数
* @param {Number} num2 浮点数
* @returns {Number}
*/
exactMath.div = function (...args) {
if (args.length === 2) {
const num1 = args[0];
const num2 = args[1];
const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));
return this.toFixed(num1 * m) / this.toFixed(num2 * m);
} else {
return args.reduce((a, b) => this.div(a, b))
}
};
/**
* 取余
* @param {Number} num1 浮点数
* @param {Number} num2 浮点数
* @returns {Number}
*/
exactMath.rem = function (...args) {
if (args.length === 2) {
const num1 = args[0];
const num2 = args[1];
const m = Math.pow(10, Math.max(this.getDecimalPlace(num1), this.getDecimalPlace(num2)));
return this.toFixed(num1 * m) % this.toFixed(num2 * m) / m;
} else {
return args.reduce((a, b) => this.rem(a, b))
}
};
/**
* n次方,仅支持整数次方(正负都可以)
* @param {Number} num 浮点数
* @param {Number} n 整数
*/
exactMath.pow = function (num, n) {
if (num == 0 && n == 0) {
return 1;
}
if (num == 0 && n > 0) {
return 0
}
if (num == 0 && n < 0) {
return Infinity;
}
// num为负数,n为负小数,返回NaN
if (num < 0 && n < 0 && Math.round(n) != n) {
return NaN;
}
if (Math.round(n) != n) {
throw new Error('n must be an integer');
}
let result = 1;
if (n > 0) {
for (let index = 0; index < n; index++) {
result = this.mul(result, num);
}
} else if (n < 0) {
for (let index = 0, len = Math.abs(n); index < len; index++) {
result = this.div(result, num);
}
}
return result;
};
/**
* 开方运算【牛顿迭代法】
*
* @param {Number} n
* @returns
*/
exactMath.sqrt = function (n) {
if (n < 0) return NaN;
if (n === 0) return 0;
if (n === 1) return 1;
let last = 0;
let res = 1;
let c = 50;
while (res != last && --c >= 0) {
last = res;
res = this.div(this.add(res, this.div(n, res)), 2)
}
return res;
// float InvSqrt(float x)
// {
// float xhalf = 0.5f * x;
// int i = * (int *) & x; // get bits for floating VALUE
// i = 0x5f375a86 - (i >> 1); // gives initial guess y0
// x = * (float *) & i; // convert bits BACK to float
// x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
// x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
// x = x * (1.5f - xhalf * x * x); // Newton step, repeating increases accuracy
// return 1 / x;
// }
};
/****************************************************随机****************************************************/
function getSeed(seed) {
if (isNaN(seed)) {
seed = Math.floor(Math.random() * 233280);
} else {
seed = Math.floor(seed % 233280);
}
return seed;
}
let randomSeed = getSeed();
/**
* 设置随机种子
*/
exactMath.setSeed = function (seed) {
randomSeed = getSeed(seed);
};
/**
* 随机
*/
exactMath.random = function () {
randomSeed = (randomSeed * 9301 + 49297) % 233280;
return randomSeed / 233280.0;
};
/**
* 根据随机种子随机
* @param {number} seed
*/
exactMath.randomBySeed = function (seed) {
seed = getSeed(seed);
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280.0;
};
/****************************************************角度弧度转换****************************************************/
/**
* 弧度数转角度数
* @param {Number} radians 浮点数
* @returns {Numbe} 浮点数
*/
exactMath.radiansToDegrees = function (radians) {
return this.div(radians, RAD);
};
/**
* 角度数转弧度数
* @param {Number} degrees 浮点数
* @returns {Numbe} 浮点数
*/
exactMath.degreesToRadians = function (degrees) {
return this.div(degrees, DEG);
};
/**
* 将角度值转换到[0, 360)范围内
* @param {Number} angle 浮点数
* @returns {Number} 整数
*/
exactMath.get0To360Angle = function (angle) {
if (angle === 0) {
return 0;
} else if (angle < 0) {
return this.add(this.rem(angle, 360), 360);
} else {
return this.rem(angle, 360);
}
};
/****************************************************三角函数****************************************************/
/**
* 查表
*/
exactMath._sin = {};
exactMath._cos = {};
exactMath._tan = {};
/**
* 3个三角函数,根据需求自行添加
* 为了效率,应该尽量使用查表法
* 表内查不到的,目前使用系统方法的结果并取前4位小数
*/
exactMath.sin = function (x) {
if (this._sin.hasOwnProperty(x)) {
return this._sin[x];
}
// if (x == 0) {
// return 0;
// } else if (x == 90) {
// return 1;
// }
// let n = x, sum = 0, i = 1;
// do {
// i++;
// sum = this.add(sum, n);
// // n = -n * x * x / (2 * i - 1) / (2 * i - 2);
// n = this.div(this.mul(-1, n, x, x), this.sub(this.mul(2, i), 1), this.sub(this.mul(2, i), 2));
// } while (Math.abs(n) >= ACCURACY_SIN_ERROR);
// return sum;
return this.toFixed(Math.sin(x), 4);
};
exactMath.cos = function (x) {
if (this._cos.hasOwnProperty(x)) {
return this._cos[x];
}
return this.toFixed(Math.cos(x), 4);
};
exactMath.tan = function (x) {
if (this._tan.hasOwnProperty(x)) {
return this._tan[x];
}
return this.toFixed(Math.tan(x), 4);
};
使用
eMath.value(10).add(20.123).mul(2).sqrt().value
eMath.value(0.1).add(0.2).value
性能测试
// 在nodejs化境下与decimal对比(其它库我没用过,可以自己测试下)
var eMath = require('./eMath');
var decimal = require('./decimal');
var time = Date.now();
for (var i = 0; i < 1000000; i++) {
eMath.value(1000.456789).add(200.123456).mul(100).div(2).add(100).sub(2).div(2).value;
}
var eTime = Date.now() - time;
time = Date.now();
for (var i = 0; i < 1000000; i++) {
decimal(1000.456789).add(200.123456).mul(100).div(2).add(100).sub(2).div(2).toNumber();
}
var dTime = Date.now() - time;
console.log(`eMath: ${eTime}ms`);
console.log(`decimal: ${dTime}ms`);
console.log(`fast: ${(dTime - eTime) / dTime * 100}%`);
// 测了5次的结果
eMath: 690ms
decimal: 3212ms
fast: 78.51805728518057%
eMath: 671ms
decimal: 2881ms
fast: 76.7094758764318%
eMath: 654ms
decimal: 3014ms
fast: 78.30126078301261%
eMath: 706ms
decimal: 3071ms
fast: 77.01074568544448%
eMath: 691ms
decimal: 3243ms
fast: 78.69256860931236%
2020/06/06 更新:
【优化】去除所有字符串操作,性能提升30%