# 数字计算
关于浮点数的存储和计算,会丢失精度这个事情就不说了, 这块需要去了解计算机是怎么存储浮点数,以及为什么会产生这个精度问题的,我们就讨论一下我们js里面常遇到的问题,以及一些解决办法即可。
比如我们经常做加法遇到, 非常常见的一个问题
0.1 + 0.2 => 0.30000000000000004
我们现在就是要解决如何正确的获取到0.3这个结果即可。 实用为主。具体的办法还真得去好好看看,这个文章就提供2个三方库解决这个问题。
# 1、number-precision
一个是比较轻量级的number-precision
const NP = require('number-precision');
NP.strip(0.09999999999999998); // = 0.1
NP.plus(0.1, 0.2); // = 0.3, not 0.30000000000000004
NP.plus(2.3, 2.4); // = 4.7, not 4.699999999999999
NP.minus(1.0, 0.9); // = 0.1, not 0.09999999999999998
NP.times(3, 0.3); // = 0.9, not 0.8999999999999999
NP.times(0.362, 100); // = 36.2, not 36.199999999999996
NP.divide(1.21, 1.1); // = 1.1, not 1.0999999999999999
NP.round(0.105, 2); // 0.11
保证一般简单的加减乘除没问题。
# 2、BigNumber
接下来学习下BigNumber (opens new window),他的API特别多, 我们可以先学会最基础的加减乘除
# 加减乘除幂
const x = BigNumber(0.1);
const y = BigNumber(0.2);
// 加法
console.log(x.plus(y).toString());
console.log(x.plus(y).toNumber());
const z = BigNumber(0.3);
// 减法
console.log(z.minus(0.1).toNumber()); // 0.2
// 乘法
console.log(z.times(y).toNumber()); // 0.06
// 除法
console.log(z.div(y).toNumber()); // 1.5
// 指数
const x1 = new BigNumber(0.7)
console.log(x1.pow(2).toNumber()); // 0.49
# 四舍五入取整操作
| rm | value | desc | desc-cn |
|---|---|---|---|
| ROUND_UP | 0 | Rounds away from zero | 远离0取值, 也就是正数向上取整 负数向下取整 |
| ROUND_DOWN | 1 | Rounds towards zero | 趋向0取值, 也就是正数向下取整 负数向上取整 |
| ROUND_CEIL | 2 | Rounds towards Infinity | 向上取值 |
| ROUND_FLOOR | 3 | Rounds towards -Infinity | 向下取值 |
| ROUND_HALF_UP | 4 | Rounds towards nearest neighbour. If equidistant, rounds away from zero | 四舍五入取值, 如果等距就远离0 |
| ROUND_HALF_DOWN | 5 | Rounds towards nearest neighbour. If equidistant, rounds towards zero | 四舍五入取值, 如果等距就趋向0 |
| ROUND_HALF_EVEN | 6 | Rounds towards nearest neighbour. If equidistant, rounds towards even neighbour | 四舍五入取值, 如果等距就取偶数值 |
| ROUND_HALF_CEIL | 7 | Rounds towards nearest neighbour. If equidistant, rounds towards Infinity | 四舍五入取值,向下取值 |
| ROUND_HALF_FLOOR | 8 | Rounds towards nearest neighbour. If equidistant, rounds towards -Infinity | 四舍五入取值,向上取值 |
// 四舍五入取整
const BigNumber = require('bignumber.js');
BigNumber.config({ ROUNDING_MODE: 1 });
// ROUNDING_MODE
const x = new BigNumber(123.456);
// BigNumber.ROUND_CEIL 向上取整
console.log(x.integerValue(BigNumber.ROUND_CEIL).toNumber()); // 124
// BigNumber.ROUND_FLOOR 向下取整
console.log(x.integerValue(BigNumber.ROUND_FLOOR).toNumber()); // 123
// BigNumber.ROUND_UP 远离0取整, 也就是正数向上取整 负数向下取整
const a1 = BigNumber(10.4);
const a2 = BigNumber(-10.4);
console.log(a1.integerValue(BigNumber.ROUND_UP).toNumber()); // 11
console.log(a2.integerValue(BigNumber.ROUND_UP).toNumber()); // -11
// BigNumber.ROUND_DOWN 趋向0取整, 也就是正数向下取整 负数向上取整
const b1 = BigNumber(10.4);
const b2 = BigNumber(-10.4);
console.log(b1.integerValue(BigNumber.ROUND_DOWN).toNumber()); // 10
console.log(b2.integerValue(BigNumber.ROUND_DOWN).toNumber()); // -10
// BigNumber.ROUND_HALF_UP 就近取整, 如果等距就远离0
const c1 = BigNumber(10.3);
const c2 = BigNumber(-10.6);
console.log(c1.integerValue(BigNumber.ROUND_HALF_UP).toNumber()); // 10
console.log(c2.integerValue(BigNumber.ROUND_HALF_UP).toNumber()); // -11
// BigNumber.ROUND_HALF_DOWN 就近取整, 如果等距就趋于0
const d1 = BigNumber(10.3);
const d2 = BigNumber(-10.5);
console.log(d1.integerValue(BigNumber.ROUND_HALF_DOWN).toNumber()); // 10
console.log(d2.integerValue(BigNumber.ROUND_HALF_DOWN).toNumber()); // -10
// BigNumber.ROUND_HALF_EVEN 就近取整, 如果等距就取偶数整
console.log(BigNumber(10.5).integerValue(BigNumber.ROUND_HALF_EVEN).toNumber()); // 10
console.log(BigNumber(-11.5).integerValue(BigNumber.ROUND_HALF_EVEN).toNumber()); // -12
// BigNumber.ROUND_HALF_CEIL 就近取整, 如果等距向上取整
// BigNumber.ROUND_HALF_DOWN 就近取整, 如果等距向下取整
# 其他实用操作
// 绝对值
const v = new BigNumber(-0.8);
console.log(v.abs().toNumber()); // 0.8
// 比较
const v1 = new BigNumber(1);
const v2 = new BigNumber(2);
console.log(v1.comparedTo(v2)); // comparedTo 可以理解为 >=, v1 >= v2, 大于返回1 小于返回-1 等于返回0
// 判断相等
const v3 = BigNumber(0.3);
const v4 = BigNumber(0.3);
console.log(v3.isEqualTo(0.3)); // true
console.log(v3.isEqualTo(v3)); // true
console.log(v3.isEqualTo(v4)); // true
// 判断大于
console.log(BigNumber(0.3).gt(BigNumber(0.2))); // true
console.log(BigNumber(0.2).gt(BigNumber(0.2))); // false
// 大于等于
console.log(BigNumber(0.3).gte(BigNumber(0.2))); // true
console.log(BigNumber(0.2).gte(BigNumber(0.2))); // true
// 小于 小于等于 lt lte
// 开方运算
console.log(Math.sqrt(0.04));
console.log(BigNumber(0.04).sqrt().toNumber()); // 0.2
# 数字显示API
// toExponential 输出指数形式
console.log(BigNumber('1000003131230000').toExponential()); // 1.00000313123e+15
// toString 在一定范围内输出数字形式,超出返回指数形式
console.log(BigNumber('1000003131230000').toString()); //1000003131230000
console.log(BigNumber('100000313123000000000000000').toString()); //1.00000313123e+26
// toFixed 总是输出数字形式
console.log(BigNumber('100000313123000000000000000').toFixed()); // 100000313123000000000000000
// 保留小数位数, HALF
console.log(BigNumber('123.456').toFixed(2, BigNumber.ROUND_FLOOR)); // 123.45
console.log(BigNumber('123.451').toFixed(2, BigNumber.ROUND_CEIL)); // 123.46
console.log(BigNumber('123.451').toFixed(2, BigNumber.ROUND_HALF_DOWN)); // 123.45
console.log(BigNumber('123.455').toFixed(2, BigNumber.ROUND_HALF_UP)); // 123.46
// toFormat 格式化
console.log(BigNumber('100000313123000000000000000').toFormat(3)); // 100,000,313,123,000,000,000,000,000.000
// toNumber
console.log(BigNumber(0.1).plus(0.2).toNumber()); // 数字 0.3, 不是字符串
个人觉得 BigNumber 在处理一般的业务时, 足够使用了。毕竟实用为主么
# 3、轻量化简易加减乘除, 会丢失精度
一个比较简单的加减乘除,能满足一般的要求,参数最好传入字符串,应该都是字符串的一些操作。 但是肯定还有一些问题的,比如参数传入0.00000001,toString之后就变成了'1e-8'的科学计数形式,
function mul(a, b) {
let c = 0,
d = a.toString(),
e = b.toString();
try {
c += d.split('.')[1].length;
} catch (f) {}
try {
c += e.split('.')[1].length;
} catch (f) {}
return (Number(d.replace('.', '')) * Number(e.replace('.', ''))) / Math.pow(10, c);
}
function add(a, b) {
let c, d, e;
try {
c = a.toString().split('.')[1].length;
} catch (f) {
c = 0;
}
try {
d = b.toString().split('.')[1].length;
} catch (f) {
d = 0;
}
e = Math.pow(10, Math.max(c, d));
return (mul(a, e) + mul(b, e)) / e;
}
function sub(a, b) {
let c, d, e;
try {
c = a.toString().split('.')[1].length;
} catch (f) {
c = 0;
}
try {
d = b.toString().split('.')[1].length;
} catch (f) {
d = 0;
}
e = Math.pow(10, Math.max(c, d));
return (mul(a, e) - mul(b, e)) / e;
}
function divide(a, b) {
var c,
d,
e = 0,
f = 0;
try {
e = a.toString().split('.')[1].length;
} catch (g) {
e = 0;
}
try {
f = b.toString().split('.')[1].length;
} catch (g) {
f = 0;
}
c = Number(a.toString().replace('.', ''));
d = Number(b.toString().replace('.', ''));
return mul(c / d, Math.pow(10, f - e));
}
// 四舍五入保留小数位数
function round(num, precision) {
const base = Math.pow(10, precision);
return (Math.round((num.toPrecision(17) * base).toFixed(1)) / base).toFixed(precision);
}
// 截位小数部分 intercept(0.1365, 2) => '0.13', 不进行4舍五入操作
function intercept(num, len) {
let a, b;
try {
const c = num.toString().split('.');
a = c[0] || '';
b = c[1] || '';
} catch (error) {
a = '';
b = '';
}
b = b.substring(0, len);
return toNonExponential([a, b].filter(Boolean).join('.'));
}
/**
* 把一个数字转换为非科学计数显示的字符串
* 超出精度的还是有问题的
* @param {string | number} num
* @return {string}
*/
function toNonExponential(num) {
if (typeof num === 'string') {
num = num - 0;
}
var m = num.toExponential().match(/\d(?:\.(\d*))?e([+-]\d+)/);
return num.toFixed(Math.max(0, (m[1] || '').length - m[2]));
}
/**
* 列表求和
*
* @param {Array<number | string>} [args=[]]
* @return {string}
*/
function sum(...args) {
console.log(args);
args = [...args].filter(num => {
if (typeof num === 'string') {
num = num.trim();
}
return Boolean(num);
});
let _sumVal = args.reduce((prev, cur) => {
return add(prev, cur);
}, 0);
return toNonExponential(_sumVal);
}
// console.log(sum(...[1, 2, 3, 4, 0.1, 0.2, '', 0, false, ' ']));
export { mul, add, sub, divide, round, intercept, sum, toNonExponential };
# 4、字符串的加减乘除。除法可以丢失精度
class LightBigNumber {
constructor(value) {
this.value = String(value).trim() || '0';
}
add(other) {
return new LightBigNumber(addStrings(this.value, String(other)));
}
sub(other) {
return new LightBigNumber(subStrings(this.value, String(other)));
}
mul(other) {
return new LightBigNumber(mulStrings(this.value, String(other)));
}
div(other, precision = 10) {
return new LightBigNumber(divStrings(this.value, String(other), precision));
}
toString() {
return this.value;
}
toNumber() {
return Number(this.value);
}
}
// 字符串加法(精确)
function addStrings(a, b) {
if (a.startsWith('-') && b.startsWith('-')) return '-' + addStrings(a.slice(1), b.slice(1));
if (a.startsWith('-')) return subStrings(b, a.slice(1));
if (b.startsWith('-')) return subStrings(a, b.slice(1));
const [intA, decA = ''] = a.split('.');
const [intB, decB = ''] = b.split('.');
const maxDec = Math.max(decA.length, decB.length);
let carry = 0;
let result = '';
// 小数部分
for (let i = maxDec - 1; i >= 0; i--) {
const sum = Number(decA[i] || 0) + Number(decB[i] || 0) + carry;
result = (sum % 10) + result;
carry = Math.floor(sum / 10);
}
// 整数部分
let i = intA.length - 1,
j = intB.length - 1;
while (i >= 0 || j >= 0 || carry) {
const sum = Number(intA[i] || 0) + Number(intB[j] || 0) + carry;
result = (sum % 10) + result;
carry = Math.floor(sum / 10);
i--;
j--;
}
if (maxDec > 0) {
result = result.slice(0, -maxDec) + '.' + result.slice(-maxDec);
}
return result.replace(/^0+\.|\.0+$/g, '').replace(/^0+/, '0') || '0';
}
// 字符串减法(精确)
function subStrings(a, b) {
if (a.startsWith('-') && b.startsWith('-')) return subStrings(b.slice(1), a.slice(1));
if (a.startsWith('-')) return '-' + addStrings(a.slice(1), b);
if (b.startsWith('-')) return addStrings(a, b.slice(1));
if (compare(a, b) < 0) return '-' + subStrings(b, a);
const [intA, decA = ''] = a.split('.');
const [intB, decB = ''] = b.split('.');
const maxDec = Math.max(decA.length, decB.length);
let borrow = 0;
let result = '';
for (let i = maxDec - 1; i >= 0; i--) {
let diff = Number(decA[i] || 0) - Number(decB[i] || 0) - borrow;
if (diff < 0) {
diff += 10;
borrow = 1;
} else {
borrow = 0;
}
result = diff + result;
}
let i = intA.length - 1,
j = intB.length - 1;
while (i >= 0) {
let diff = Number(intA[i]) - (j >= 0 ? Number(intB[j]) : 0) - borrow;
if (diff < 0) {
diff += 10;
borrow = 1;
} else {
borrow = 0;
}
result = diff + result;
i--;
j--;
}
result = result.replace(/^0+/, '') || '0';
if (maxDec > 0) {
const intPart = result.slice(0, -maxDec) || '0';
const decPart = result.slice(-maxDec).replace(/0+$/, '');
return decPart ? `${intPart}.${decPart}` : intPart;
}
return result;
}
// 字符串乘法(精确)
function mulStrings(a, b) {
let sign = '';
if ((a.startsWith('-') && !b.startsWith('-')) || (!a.startsWith('-') && b.startsWith('-')))
sign = '-';
a = a.replace(/^-/, '');
b = b.replace(/^-/, '');
const decA = a.split('.')[1] || '';
const decB = b.split('.')[1] || '';
const totalDecimals = decA.length + decB.length;
const intA = a.replace('.', '');
const intB = b.replace('.', '');
const result = Array(intA.length + intB.length).fill(0);
for (let i = intA.length - 1; i >= 0; i--) {
for (let j = intB.length - 1; j >= 0; j--) {
const mul = Number(intA[i]) * Number(intB[j]) + result[i + j + 1];
result[i + j + 1] = mul % 10;
result[i + j] += Math.floor(mul / 10);
}
}
let str = result.join('').replace(/^0+/, '') || '0';
if (totalDecimals > 0) {
if (str.length <= totalDecimals) {
str = '0'.repeat(totalDecimals - str.length + 1) + str;
}
str = str.slice(0, -totalDecimals) + '.' + str.slice(-totalDecimals);
str = str.replace(/\.?0+$/, '');
}
return sign + str;
}
// 字符串除法(支持精度控制,四舍五入)
function divStrings(a, b, precision = 10) {
let sign = '';
if ((a.startsWith('-') && !b.startsWith('-')) || (!a.startsWith('-') && b.startsWith('-')))
sign = '-';
a = a.replace(/^-/, '');
b = b.replace(/^-/, '');
if (b === '0') return 'NaN';
if (a === '0') return '0';
// 标准化为整数
const [intA, decA = ''] = a.split('.');
const [intB, decB = ''] = b.split('.');
const decDiff = decA.length - decB.length;
let dividend = intA + decA;
let divisor = intB + decB;
if (decDiff < 0) dividend += '0'.repeat(-decDiff);
if (decDiff > 0) divisor += '0'.repeat(decDiff);
dividend = dividend.replace(/^0+/, '') || '0';
divisor = divisor.replace(/^0+/, '') || '0';
// 长除法
let quotient = '';
let remainder = '';
let idx = 0;
let current = '';
while (idx < dividend.length && compare(current, divisor) < 0) {
current += dividend[idx++];
}
if (compare(current, divisor) < 0) {
quotient = '0';
remainder = current;
} else {
while (idx <= dividend.length) {
let digit = 0;
for (let d = 9; d >= 0; d--) {
if (compare(mulStrings(divisor, String(d)), current) <= 0) {
digit = d;
break;
}
}
quotient += digit;
remainder = subStrings(current, mulStrings(divisor, String(digit)));
if (idx < dividend.length) remainder += dividend[idx];
idx++;
current = remainder;
}
}
// 小数部分
let decimalPart = '';
if (precision > 0) {
let decRemainder = remainder;
for (let i = 0; i < precision + 1; i++) {
if (decRemainder === '0') break;
decRemainder += '0';
let digit = 0;
for (let d = 9; d >= 0; d--) {
if (compare(mulStrings(divisor, String(d)), decRemainder) <= 0) {
digit = d;
break;
}
}
decimalPart += digit;
decRemainder = subStrings(decRemainder, mulStrings(divisor, String(digit)));
}
// 四舍五入
if (decimalPart.length > precision) {
const lastDigit = Number(decimalPart[precision]);
decimalPart = decimalPart.slice(0, precision);
if (lastDigit >= 5) {
let decNum = Number(decimalPart) + 1;
if (decNum.toString().length > precision) {
quotient = addStrings(quotient, '1');
decimalPart = decNum.toString().slice(1);
} else {
decimalPart = decNum.toString().padStart(precision, '0');
}
}
}
}
let result = quotient + (decimalPart ? '.' + decimalPart : '');
result = result.replace(/^0+\./, '0.').replace(/\.0+$/, '');
if (result.startsWith('0.') && result.length > 2) {
result = result.replace(/^0+/, '');
}
return sign + result;
}
// 比较两个正整数字符串
function compare(a, b) {
a = a.replace(/^0+/, '');
b = b.replace(/^0+/, '');
if (a.length > b.length) return 1;
if (a.length < b.length) return -1;
return a.localeCompare(b);
}
export { LightBigNumber, addStrings, subStrings, mulStrings, divStrings, compare };
使用示例
// 使用示例
const n1 = new LightBigNumber('0.1');
console.log(n1.add('0.2').toString()); // "0.3"
console.log(n1.mul('0.1').toString()); // "0.01"
console.log(n1.div('3').toString()); // "0.03333333333333333"
const n2 = new LightBigNumber('12345678901234567890.123456789');
console.log(n2.add('1').toString()); // "12345678901234567891.123456789"
console.log(n2.mul('2').toString()); // "24691357802469135780.246913578"
const n3 = new LightBigNumber('9007199254740992')
console.log(n3.add('7').toString());
console.log(addStrings('9007199254740992', '9007199254743453412312312312312353534534535353450992'));
console.log(mulStrings('9007199254740992', '9007199254740992'));