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
使用构造函数前面可以不带 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 && 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
parse 函数会为实例对象添加三个属性;
-
x.s
,表示数字的符号,即是正数还是负数,即正负值,若是正数,x.s = 1,负数则为 -1 -
x.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 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 确定数字是否有前置 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 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
- 如果符号不同,则转为减法运算;比如 -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
- 接下来是加法逻辑
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 [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]
减法前面的逻辑和加法类似,这里不再赘述,已在上面代码注释中说明,下面是减法的核心逻辑
// 从 被减数 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] [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 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 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 rl ? 1 : -1;
} else {
for (ri = -1, cmp = 0; ++ri r[ri] ? 1 : -1;
break;
}
}
}
// If divisor p) round(q, p, Big.RM, r[0] !== UNDEFINED);
return q;
};
在除法运算中,对于 a/b , a 是被除数,b 是除数,下面依次分析上面代码
-
if (dp !== ~~dp || 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 rl ? 1 : -1;
} else {
for (ri = -1, cmp = 0; ++ri r[ri] ? 1 : -1;
break;
}
}
}
// 除数小于余数,则继续从余数中减去除数
if (cmp
这个循环做了这些事情:以 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 解析函数源码,四则运算源码,分别用图文详细描述了运算过程,一步步还原了作者的构思。有不正确的地方或者不同见解还请各位大佬提出来。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net