图文剖析 big.js 四则运算源码
big.js,一个小型、快速的用于任意精度的十进制算术的JavaScript 库。
big.js 用于解决平常项目中进行算术运算时精度丢失引起的结果不准确的问题。和 big.js 类似的两个库 bignumber.js 和 decimal.js 也都是出自同一作者(MikeMcl)之手。
作者在 这里 详细说明了他们之间的区别
big.js
是最小的任意精度的计算库。big.js
是三者中最小也最简单的,它只有bignumber.js
一半的方法,不到bignumber.js
的一半大。
bignumber.js
和decimal.js
存储值的进制比big.js
更高,因此当操作大量数字时,前两者的速度会更快。
bignumber.js
可能更适合金融类应用,因为用户不用担心丢失精度,除非使用了涉及除法的操作。
这篇文章分别就 big.js 的解析函数,以及加减乘除运算的源码进行剖析,了解作者的设计思路。在四则运算的源码中,相比加减乘,除法运算最为复杂。
用法
创建 Big 对象时,new 操作符是可选的
x = new Big(123.4567)
y = Big('123456.7e-3') // 'new' is optional
z = new Big(x)
x.eq(y) && x.eq(z) && y.eq(z) // true
构造函数
构造函数中关键代码如下
function Big(n) {
var x = this;
// 使用构造函数前面可以不带 new 关键字
if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n);
// 如果传进来的参数已经是 Big 的实例对象,则复制一份,否则使用 parse 函数创建一个实例对象
if (n instanceof Big) {
x.s = n.s;
x.e = n.e;
x.c = n.c.slice();
} else {
if (typeof n !== 'string') {
if (Big.strict === true && typeof n !== 'bigint') {
throw TypeError(INVALID + 'value');
}
// 传入的如果是 -0 ,则转为字符串表示 '-0'
n = n === 0 && 1 / n < 0 ? '-0' : String(n);
}
parse(x, n);
}
使用构造函数前面可以不带 new 关键字
如果传进来的参数已经是 Big 的实例对象,则将实例对象的属性复制一份,否则使用 parse 函数为实例对象创建属性。
parse 函数
function parse(x, n) {
var e, i, nl;
// NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
if (!NUMERIC.test(n)) {
throw Error(INVALID + 'number');
}
// Determine sign.
x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1;
// Decimal point?
if ((e = n.indexOf('.')) > -1) n = n.replace('.', '');
// Exponential form?
if ((i = n.search(/e/i)) > 0) {
// Determine exponent.
if (e < 0) e = i;
e += +n.slice(i + 1);
n = n.substring(0, i);
} else if (e < 0) {
// Integer.
e = n.length;
}
nl = n.length;
// Determine leading zeros.
for (i = 0; i < nl && n.charAt(i) == '0';) ++i;
if (i == nl) {
// Zero.
x.c = [x.e = 0];
} else {
// Determine trailing zeros.
for (; nl > 0 && n.charAt(--nl) == '0';);
x.e = e - i - 1;
x.c = [];
// Convert string to array of digits without leading/trailing zeros.
for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
}
return x;
}
parse 函数会为实例对象添加三个属性;
x.s
,表示数字的符号,即是正数还是负数,即正负值,若是正数,x.s = 1,负数则为 -1x.e
,表示数字对应的指数表示法的指数,比如n = 1234
的指数为3
x.c
,数字数组,比如1234
转换后是 [1,2,3,4]
1234 会被转化为
{
c:[1,2,3,4],
e:3,
s:1
}
这种表示,和 IEEE 754 双精度浮点数的存储方式 很类似,而 JavaScript 的 Number类型就是一个双精度 64 位二进制格式 IEEE 754 值使用 64 位来表示 3 个部分:
- 1 位用于表示符号(sign) (正数或者负数)
- 11 位用于表示指数(exponent) (-1022 到 1023)
- 52 位用于表示尾数(mantissa) (表示 0 和 1 之间的数值)
下面分析 parse 函数转化的详细过程,以 Big('123400')
,Big('0.1234')
,Big('100e2')
为例
注意:Big('100e2') 中 100e2 以字符串形式传进来才能检测到 e ,Number形式的 Big(100e2),执行 parse 前会被转化为 Big(10000)
- 校验传入的值,只允许数字,
'.1'
,指数形式的写法。比如2.34
,.2
,10e2
// NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
if (!NUMERIC.test(n)) {
throw Error(INVALID + 'number');
}
Big('123400'),Big('-0.1234'),Big('100e2') 都通过
x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1;
确定符号
Big('123400') => x.s = 1
Big('-0.1234') => x.s = -1 并且 -0.1234 => 0.1234
Big('100e2') => x.s = 1
if ((e = n.indexOf('.')) > -1) n = n.replace('.', '');
是否含有小数点,如果是,则删除小数点,并将 e 的初始值设为小数点的位置
Big('123400') => x.e = -1 , n = 123400
Big('-0.1234') => x.e = 1 , n = 01234
Big('100e2') => x.e = -1 , n = 100e2
- 如果数字是科学表示法,比如
100e2
,e 的位置是 3,e 后面的指数是 2 ,则 x.e = 3 + 2
if ((i = n.search(/e/i)) > 0) {
// Determine exponent.
if (e < 0) e = i;
e += +n.slice(i + 1);
n = n.substring(0, i);
} else if (e < 0) {
// Integer.
e = n.length;
}
Big('123400')
x.e = -1 => x.e = n.length = 6
n = 123400
Big('-0.1234')
x.e = 1
n = 01234
Big('100e2')
x.e = -1 => x.e = e 在 100e2 中的位置 + e 后面紧跟的指数系数 = 3 + 2 = 5
n = 100e2 => n = 100
nl = n.length;
nl 表示传进来的数字的长度
Big('123400')
x.e = 6
n = 123400
nl = 6
Big('-0.1234')
x.e = 1
n = 01234
nl = 5
Big('100e2')
x.e = 5
n = 100
nl = 3
for (i = 0; i < nl && n.charAt(i) == '0';) ++i;
确定数字是否有前置 0 ,这里的 i 表示第一个不为 0 的数字的位置,也可以表示数字前面有多少个 0
Big('123400')
x.e = 6
n = 123400
nl = 6
i = 0
Big('-0.1234')
x.e = 1
n = 01234
nl = 5
i = 1
Big('100e2')
x.e = 5
n = 100
nl = 3
i = 0
- 如果 i = nl,则说明传进来的输入是一个 0 或者多个 0
if (i == nl) {
// Zero.
x.c = [x.e = 0];
} else {
// 排除尾随 0,nl 为最后一个不为 0 的数字的位置
for (; nl > 0 && n.charAt(--nl) == '0';);
x.e = e - i - 1;
x.c = [];
// 传进来的数字,排除掉前置 0 和尾随 0 后,转换为数字数组
for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
}
Big('123400')
//因为默认 e 是 n.length,而Big指数表示是 1.234 * 10^5,所以这里 x.e 要减一
x.e = 6 => x.e = e - i - 1 = 6 - 0 - 1 = 5
n = 123400
nl = 6 => 排除尾随 0,nl 为最后一个不为 0 的数字的位置 => nl = 3
i = 0
x.c => 选取 n 中从 i 到 nl 的数字组成数组 [1,2,3,4]
Big('-0.1234')
x.e = 1 => x.e = e - i - 1 = 1 - 1 - 1 = -1
n = 01234
nl = 5 => 排除尾随 0,nl 为最后一个不为 0 的数字的位置 => nl = 4
i = 1
x.c => 选取 n 中从 i 到 nl 的数字组成数组 [1,2,3,4]
Big('100e2')
x.e = 5 => x.e = e - i - 1 = 5 - 0 - 1 = 4
n = 100
nl = 3 => 排除尾随 0,nl 为最后一个不为 0 的数字的位置 => nl = 0
i = 0
x.c => 选取 n 中从 i 到 nl 的数字组成数组 [1]
最后 Big('123400'),Big('-0.1234'),Big('100e2') 将转换为
Big('123400')
x.s = 1
x.e = 5
x.c = [1,2,3,4]
Big('-0.1234')
x.s = -1
x.e = -1
x.c = [1,2,3,4]
Big('100e2')
x.s = 1
x.e = 4
x.c = [1]
至此 parse 函数逻辑结束,接下来分别剖析下加减乘除运算;
加法
源码
P.plus = P.add = function (y) {
var e, k, t,
x = this,
Big = x.constructor;
y = new Big(y);
// 校验符号是否不同
if (x.s != y.s) {
y.s = -y.s;
return x.minus(y);
}
var xe = x.e,
xc = x.c,
ye = y.e,
yc = y.c;
// 校验是否是 0
if (!xc[0] || !yc[0]) {
if (!yc[0]) {
if (xc[0]) {
y = new Big(x);
} else {
y.s = x.s;
}
}
return y;
}
xc = xc.slice();
// 前面加上零使指数均衡
// Note: reverse faster than unshifts.
if (e = xe - ye) {
if (e > 0) {
ye = xe;
t = yc;
} else {
e = -e;
t = xc;
}
t.reverse();
for (; e--;) t.push(0);
t.reverse();
}
// 让 xc 存放长度更长的数字
if (xc.length - yc.length < 0) {
t = yc;
yc = xc;
xc = t;
}
e = yc.length;
for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0;
// No need to check for zero, as +x + +y != 0 && -x + -y != 0
if (k) {
xc.unshift(k);
++ye;
}
// 删除尾随 0
for (e = xc.length; xc[--e] === 0;) xc.pop();
y.c = xc;
y.e = ye;
return y;
};
- 如果符号不同,则转为减法运算;比如 -x + y 就是 y - x ,x + -y 就是 x - y
if (x.s != y.s) {
y.s = -y.s;
return x.minus(y);
}
- 其中两个数字是不是 0,其中有一个为 0,则直接返回另外一个
if (!xc[0] || !yc[0]) {
if (!yc[0]) {
if (xc[0]) {
y = new Big(x);
} else {
y.s = x.s;
}
}
return y;
}
- 比较指数幂差,较小的一方,在前面补零,方便后续加法操作;并且将指数幂较大的一方,作为两数相加的结果的指数幂的初始值。
if (e = xe - ye) {
if (e > 0) {
ye = xe; // 将指数幂较大的一方,作为两数相加的结果的指数幂的初始值
t = yc;
} else {
e = -e;
t = xc;
}
t.reverse();
for (; e--;) t.push(0);
t.reverse();
}
比如 1234 + 12
1234 在实例对象上是以数字数组形式表示 [1,2,3,4]
12 则是 [1,2]
为方便后续数组按照位置进行加法运算,这里需要给 12 补零
[1,2,3,4]
+
[0,0,1,2]
- xc 存放长度更长的数字
if (xc.length - yc.length < 0) {
t = yc;
yc = xc;
xc = t;
}
- 接下来是加法逻辑
e = yc.length;
for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0;
if (k) {
xc.unshift(k);
++ye;
}
// 删除尾随 0
for (e = xc.length; xc[--e] === 0;) xc.pop();
k 保存进位的值
- 初始化进位值为 0 ,e 为 yc 长度,执行下面循环体
(xc[--e] = xc[e] + yc[e] + k)
计算 xc[e] 加上 yc[e] 加上上一次计算结果进位的值;- 随后 xc[--e] 保存计算后的进位的数值,e--
- 最后 xc[e] 保存计算后的个位数值
上面过程用图例表示如下
减法
源码
P.minus = P.sub = function (y) {
var i, j, t, xlty,
x = this,
Big = x.constructor,
a = x.s,
b = (y = new Big(y)).s;
// 确定符号,x - (-y) = x + y - x - y = -x + (-y)
if (a != b) {
y.s = -b;
return x.plus(y);
}
var xc = x.c.slice(),
xe = x.e,
yc = y.c,
ye = y.e;
// 判断是否为 0
if (!xc[0] || !yc[0]) {
if (yc[0]) {
y.s = -b;
} else if (xc[0]) {
y = new Big(x);
} else {
y.s = 1;
}
return y;
}
// 比较两数指数幂大小,给指数幂小的一方补零,方便后续相减;
// 比如 1234 - 23 parse函数解析后 => [1,2,3,4] - [2,3] 为了使 [2,3] 对应十位,个位
// 在前面补 0 ,即 [1,2,3,4] - [0,0,2,3]
// 再比如 66 - 233 parse函数解析后 => [6,7] - [2,3,3],同样为了使 6,7对应十位,个位
// 在前面补 0 ,即 [0,6,7] - [2,3,3]
if (a = xe - ye) {
if (xlty = a < 0) {
a = -a;
t = xc;
} else {
ye = xe;
t = yc;
}
t.reverse();
for (b = a; b--;) t.push(0); // 补零
t.reverse();
} else {
// 若指数幂相等,不需要补零,则比较两数大小,从最大位开始比较;
// 比如 [2,3,4] 和 [1,2,3] 最大位是百位,若百位的数字不相等,则可得出孰大孰小
j = ((xlty = xc.length < yc.length) ? xc : yc).length;
for (a = b = 0; b < j; b++) {
if (xc[b] != yc[b]) {
xlty = xc[b] < yc[b];
break;
}
}
}
// 对于被减数 x 和减数 y
// 如果 x - y < 0,则交换两数,并改变符号;比如 2 - 4 = -(4-2)
if (xlty) {
t = xc;
xc = yc;
yc = t;
y.s = -y.s;
}
// 如果被减数的数字数组长度小于减数,则给被减数的末尾添加 0
// 比如 12 - 0.0009 parse函数解析后 => [1,2] - [0,0,0,0,9]
// 因为 9 是小数后几位,相应的需要给 [1,2]末尾补 0 ,即 [1,2,0,0,0] - [0,0,0,0,9]
if ((b = (j = yc.length) - (i = xc.length)) > 0) for (; b--;) xc[i++] = 0;
// 从 xc 中减去 yc
for (b = i; j > a;) {
if (xc[--j] < yc[j]) {
for (i = j; i && !xc[--i];) xc[i] = 9;
--xc[i];
xc[j] += 10;
}
xc[j] -= yc[j];
}
// 去掉运算结果末尾 0
for (; xc[--b] === 0;) xc.pop();
// 去掉运算结果前置 0 ,并减去相应指数幂
for (; xc[0] === 0;) {
xc.shift();
--ye;
}
// 运算结果为 0 的情况
if (!xc[0]) {
// n - n = +0
y.s = 1;
xc = [ye = 0];
}
y.c = xc;
y.e = ye;
return y;
};
减法前面的逻辑和加法类似,这里不再赘述,已在上面代码注释中说明,下面是减法的核心逻辑
// 从 被减数 xc 中减去减数 yc
// a 是 xc 和 yc 的幂的差值,j 是 yc 的长度,这里循环条件用 j > a,表示循环 j-a 次
// 比如 120 - 9 => [1,2,0]-[0,0,9] 指数幂差是 2 ,减数数字数组长度是 3 ,则只需要循环 3-2=1 次
// 比如 120 - 0.009 => [1,2,0,0,0,0]-[0,0,0,0,0,9] 指数幂差是 5 ,减数数字数组长度是 6 ,则只需要循环 6-5=1 次
for (b = i; j > a;) {
if (xc[--j] < yc[j]) {
//从后往前遍历xc,当碰到值为0 ,将值改为 9;
//比如 [1,0,0]-[0,0,9] => [0,9,10] -
for (i = j; i && !xc[--i];) xc[i] = 9;
--xc[i];
xc[j] += 10;
}
xc[j] -= yc[j];
}
上面过程用图例表示如下,xc 表示被减数,yc 表示减数
1、若 xc 末尾项大于等于 yc 末尾项,比如 [1,2,3]和[0,0,2],则直接相减。
2、若 xc 末尾项小于 yc 末尾项,则执行以下逻辑
for (i = j; i && !xc[--i];) xc[i] = 9;
上面代码表示从 当前进行相减运算的元素的位置(j) 往前遍历被减数 xc 每个元素,当元素值为 0 时,将值改为 9,直至上一个元素值不为 0 ,循环结束。
至此,减法逻辑结束。
乘法
源码
P.times = P.mul = function (y) {
var c,
x = this,
Big = x.constructor,
xc = x.c,
yc = (y = new Big(y)).c,
a = xc.length,
b = yc.length,
i = x.e,
j = y.e;
// 确定结果的符号
y.s = x.s == y.s ? 1 : -1;
// 其中一个为 0 ,返回结果为 0
if (!xc[0] || !yc[0]) {
y.c = [y.e = 0];
return y;
}
// 初始化结果的指数
y.e = i + j;
// 对比 xc,yc 长度,xc 存放长度更长的一方
if (a < b) {
c = xc;
xc = yc;
yc = c;
j = a;
a = b;
b = j;
}
// 用 0 初始化结果数组
for (c = new Array(j = a + b); j--;) c[j] = 0;
// i is initially xc.length.
for (i = b; i--;) {
b = 0;
// a is yc.length.
for (j = a + i; j > i;) {
// Current sum of products at this digit position, plus carry.
b = c[j] + yc[i] * xc[j - i - 1] + b;
c[j--] = b % 10;
// carry
b = b / 10 | 0;
}
c[j] = b;
}
// 如果有最终进位,则增加结果的指数,否则删除头部的 0
if (b) ++y.e;
else c.shift();
// 删除尾部的 0
for (i = c.length; !c[--i];) c.pop();
y.c = c;
return y;
};
乘法源码的主要逻辑是下面这一段
for (i = b; i--;) {
b = 0;
for (j = a + i; j > i;) {
// 当前数字位置的总和,加上进位
b = c[j] + yc[i] * xc[j - i - 1] + b;
c[j--] = b % 10;
// 进位值
b = b / 10 | 0;
}
c[j] = b;
}
描述的其实就是以前老师教我们在纸上乘法运算的过程:
以 123*12
来举例子分析上面这段代码
xc 是乘数 [1,2,3],yc 是被乘数 [1,2],b 是 yc 长度,a 是 xc 长度
c 是保存结果的数组,定义的长度是 a+b
两个数相乘得到的结果长度可能是 a+b,也有可能是 a+b-1。所以后面需要删除数组头部的 0
for (i = b; i--;)
首先是外层循环,从数组长度较短的被乘数开始循环,将 b 赋值给 i,i 充当 yc 的长度,而 b 用来保存进位的值b = 0
定义进位的值for (j = a + i; j > i;)
内层乘数(123)的循环,这里的 j 表示在结果数组 c 中的位置
for (j = a + i; j > i;)
实际上就是 for ( j = 乘数长度 + 当前被乘数数字的位置 ),这里是因为当第二轮外层循环时,123 * 1 的时候,1 是 12 的 十位,所以在 j 也应该从十位开始保存计算结果。
第一轮外层循环
第二轮外层循环
b = c[j] + yc[i] * xc[j - i - 1] + b
当前数字位置的总和,加上进位
c[j--] = b % 10;
当前位置去整取余b = b / 10 | 0;
进位值取整
至此乘法运算逻辑结束
除法
源码
P.div = function (y) {
var x = this,
Big = x.constructor,
a = x.c, // dividend
b = (y = new Big(y)).c, // divisor
k = x.s == y.s ? 1 : -1,
dp = Big.DP;
if (dp !== ~~dp || dp < 0 || dp > MAX_DP) {
throw Error(INVALID_DP);
}
// Divisor is zero?
if (!b[0]) {
throw Error(DIV_BY_ZERO);
}
// Dividend is 0? Return +-0.
if (!a[0]) {
y.s = k;
y.c = [y.e = 0];
return y;
}
var bl, bt, n, cmp, ri,
bz = b.slice(),
ai = bl = b.length,
al = a.length,
r = a.slice(0, bl), // remainder
rl = r.length,
q = y, // quotient
qc = q.c = [],
qi = 0,
p = dp + (q.e = x.e - y.e) + 1; // precision of the result
q.s = k;
k = p < 0 ? 0 : p;
// Create version of divisor with leading zero.
bz.unshift(0);
// Add zeros to make remainder as long as divisor.
for (; rl++ < bl;) r.push(0);
do {
// n is how many times the divisor goes into current remainder.
for (n = 0; n < 10; n++) {
// Compare divisor and remainder.
if (bl != (rl = r.length)) {
cmp = bl > rl ? 1 : -1;
} else {
for (ri = -1, cmp = 0; ++ri < bl;) {
if (b[ri] != r[ri]) {
cmp = b[ri] > r[ri] ? 1 : -1;
break;
}
}
}
// If divisor < remainder, subtract divisor from remainder.
if (cmp < 0) {
// Remainder can't be more than 1 digit longer than divisor.
// Equalise lengths using divisor with extra leading zero?
for (bt = rl == bl ? b : bz; rl;) {
if (r[--rl] < bt[rl]) {
ri = rl;
for (; ri && !r[--ri];) r[ri] = 9;
--r[ri];
r[rl] += 10;
}
r[rl] -= bt[rl];
}
for (; !r[0];) r.shift();
} else {
break;
}
}
// Add the digit n to the result array.
qc[qi++] = cmp ? n : ++n;
// Update the remainder.
if (r[0] && cmp) r[rl] = a[ai] || 0;
else r = [a[ai]];
} while ((ai++ < al || r[0] !== UNDEFINED) && k--);
// Leading zero? Do not remove if result is simply zero (qi == 1).
if (!qc[0] && qi != 1) {
// There can't be more than one zero.
qc.shift();
q.e--;
p--;
}
// Round?
if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED);
return q;
};
在除法运算中,对于 a/b , a 是被除数,b 是除数,下面依次分析上面代码
if (dp !== ~~dp || dp < 0 || dp > MAX_DP)
判断 dp 是不是大于 0 的整数,并且小于 MAX_DP,这里的 dp 可以自己设置
Big.DP = 30
- 除数为 0 则抛出错误。
if (!b[0]) {
throw Error(DIV_BY_ZERO);
}
- 被除数是 0 则返回值为 0 的实例对象
if (!a[0]) {
y.s = k;
y.c = [y.e = 0];
return y;
}
- 接下来是除法运算逻辑,定义变量的那一段不贴了,直接看 do while 循环
do {
// n 是循环次数,表示从当前位置的余数中可以分出多少个除数来,也就是当前位置的商。
for (n = 0; n < 10; n++) {
// 比较除数和余数大小
if (bl != (rl = r.length)) {
cmp = bl > rl ? 1 : -1;
} else {
for (ri = -1, cmp = 0; ++ri < bl;) {
if (b[ri] != r[ri]) {
cmp = b[ri] > r[ri] ? 1 : -1;
break;
}
}
}
// 除数小于余数,则继续从余数中减去除数
if (cmp < 0) {
// Remainder can't be more than 1 digit longer than divisor.
// Equalise lengths using divisor with extra leading zero?
for (bt = rl == bl ? b : bz; rl;) {
if (r[--rl] < bt[rl]) {
ri = rl;
for (; ri && !r[--ri];) r[ri] = 9;
--r[ri];
r[rl] += 10;
}
r[rl] -= bt[rl];
}
for (; !r[0];) r.shift();
} else {
break;
}
}
// qc 数组保存商
qc[qi++] = cmp ? n : ++n;
// 更新余数
if (r[0] && cmp) r[rl] = a[ai] || 0;
else r = [a[ai]];
} while ((ai++ < al || r[0] !== UNDEFINED) && k--);
这个循环做了这些事情:以 1234 / 9 为例;
- 将当前位置的余数 1 和除数 9 比较大小,(一开始余数取的是被除数的前面 n 位,n 和除数的长度大小相同,所以取的是 1 )。先比较长度,长度相同再比较大小。
- 若除数大于当前位置余数,则跳出循环(当前循环次数即为当前位置的商,9 > 1 ,那么当前循环次数为 0 ,即当前位置商为 0 )
- 然后保存商
qc[qi++] = cmp ? n : ++n;
- 最后更新当前余数,除数大于余数时,则当前余数向后借一位,余数就由 1 变为了 12
if (r[0] && cmp) r[rl] = a[ai] || 0;
else r = [a[ai]];
- 若除数小于当前余数,则继续从余数中减去除数 (开始减法 for 循环)
- 然后再次保存商
qc[qi++] = cmp ? n : ++n;
- 然后再次更新当前余数,除数大于余数时,则当前余数向后借一位,余数就由 3 变为了 33
- 当商数组的长度没有达到指定的精度总和,继续上面的步骤,直至循环结束;
指定的精度总和指的是 Big.DP(默认20) + (被除数的指数-除数的指数); 1234 / 9 的指定精度总和是 23。
- 商数组长度大于 1 的情况下,删除数组前面的 0 ;如果是 商就是 0 ,比如 0/1 = 0,这种情况不必删除 0 了
if (!qc[0] && qi != 1) {
qc.shift();
q.e--;
p--;
}
- 舍入操作
if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED);
至此除法逻辑结束
注意事项
big.js 用数组存储值,类似 高精度计算,只不过 big.js 是数组中每个位置存储一个值,然后对每个位置进行运算;而对超级大的数字(数百或数千位数值时),big.js 算术运算不如 bignumber.js 快。
例如,bignumber.js 将数字1234.56789的数字存储为[1234,56789000000000] ,即以两个1e14为基数的数组形式存储,而 big.js 存储的数字与[1,2,3,4.5,6,7,8,9]相同,即以9个10为基数的数组形式存储。前者的算术运算可能更快,因为需要处理的元素较少。在实践中,这可能只有在使用数百或数千位数值时才会有所不同。
在使用 big.js 进行运算时需要注意有时候没有设置足够大的精度,会导致结果不是想要的。
Big.DP = 20
+Big(1).div('11111111').times('11111111') // 0.9999999999999999
// 0.9999999999999999 在 Number 编码的可以表示的准确精度范围内
Big.DP = 30
+Big(1).div('11111111').times('11111111') // 1
// 而设置 Big.DP = 30 后
//结果数组保存的是 999999999999999999999999
//超过了 Number 编码的可以表示的准确精度范围,则会舍入为 1
总结
本文剖析了 big.js 解析函数源码,四则运算源码,分别用图文详细描述了运算过程,一步步还原了作者的构思。有不正确的地方或者不同见解还请各位大佬提出来。
图文剖析 big.js 四则运算源码的更多相关文章
- 【java集合框架源码剖析系列】java源码剖析之TreeSet
本博客将从源码的角度带领大家学习TreeSet相关的知识. 一TreeSet类的定义: public class TreeSet<E> extends AbstractSet<E&g ...
- 【java集合框架源码剖析系列】java源码剖析之HashSet
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...
- [百度地图] 用于类似 DWZ UI 框架的 百度地图 功能封装类 [MultiZMap.js] 实例源码
MultiZMap 功能说明 MultiZMap.js 本类方法功能大多使用 prototype 原型 实现,它是 ZMap 的多加载版本,主要用于类似 DWZ 这个 多标签的 UI 的框架: 包含的 ...
- 从vue.js的源码分析,input和textarea上的v-model指令到底做了什么
v-model是 vue.js 中用于在表单表单元素上创建双向数据绑定,它的本质只是一个语法糖,在单向数据绑定的基础上,增加了监听用户输入事件并更新数据的功能:对,它本质上只是一个语法糖,但到底是一个 ...
- 百度地图整合功能分享修正版[ZMap.js] 实例源码!
ZMap 功能说明 ZMap 是学习百度地图 api 接口,开发基本功能后整的一个脚本类,本类方法功能大多使用 prototype 原型 实现: 包含的功能有:轨迹回放,圈画区域可编辑,判断几个坐标是 ...
- 【 js 模块加载 】深入学习模块化加载(node.js 模块源码)
一.模块规范 说到模块化加载,就不得先说一说模块规范.模块规范是用来约束每个模块,让其必须按照一定的格式编写.AMD,CMD,CommonJS 是目前最常用的三种模块化书写规范. 1.AMD(Asy ...
- 【java集合框架源码剖析系列】java源码剖析之TreeMap
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于TreeMap的知识. 一TreeMap的定义: public class TreeMap&l ...
- 【java集合框架源码剖析系列】java源码剖析之ArrayList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...
- 【java集合框架源码剖析系列】java源码剖析之LinkedList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 在实际项目中LinkedList也是使用频率非常高的一种集合,本博客将从源码角度带领大家学习关于LinkedList的知识. ...
- 【java集合框架源码剖析系列】java源码剖析之HashMap
前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...
随机推荐
- 【笔记】- 【美团1万台 Hadoop 集群 YARN 的调优之路】
原文:美团1万台 Hadoop 集群 YARN 的调优之路 背景 架构 YARN架构 资源抽象 YARN调度架构 资源抽象 调度流程 作业的组织方式 核心调度流程 指标 业务指标:有效调度 系统性能指 ...
- 使用lame转wav为mp3
使用lame转wav为mp3 由于服务器之前都是直接存储wav格式的录音文件,存储空间总是不够用.网上搜索了一下,可以使用lame这个开源工具,将wav转成MP3 格式,还可以将mp3文件转回wav格 ...
- iptables基础
简介 Linux防火墙主要工作在网络层,针对TCP/IP数据包实施过滤和限制.iptables是linux防火墙的管理程序,为包过滤机制的实现制定规则,并告诉内核空间的netfilter如何处理这些网 ...
- Programming abstractions in C阅读笔记:p107-p110
<Programming Abstractions In C>学习第46天,p107-p110,3.1小节--"The concept of interface",总结 ...
- PHP对关联数组(键值对数组)遍历循环
PHP对关联数组循环遍历 $arr=array('yxb'=>20,'ylg'=>21,'lgj'=18); foreach($arr as $name=>$value) { ech ...
- 月工资不到10元的内容审核专员? - ChatGPT 在内容自动审查中的应用
内容过滤筛查是指对网络上发布或传播的文本.图片.视频等内容进行审核和监管,以防止出现违法违规.暴力色情.虚假广告.电信诈骗等现象,维护网络安全和社会秩序. 内容过滤筛查是一个亟待解决的问题,因为网络内 ...
- 2016A
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <algorithm& ...
- Solution Set -「ABC 205」
应该是最近最水的 ABC 了吧. 「ABC 205A」kcal Link. 略 #include <bits/stdc++.h> using ll = long long; #define ...
- tcpdump后台不间断抓包
版本1的抓包命令 这两天排查一个小问题,需要在服务器上使用tcpdump24小时不间断抓包,这里简单记录下. 先看下tcpdump的语法: tcpdump [ -AbdDefhHIJKlLnNOpqS ...
- Hadoop - WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform...
Hadoop - WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... 配置完ha ...