Decimal.js计算逻辑

Decimal.js 原理

JavaScript 中的数字类型是基于 IEEE 754 标准来实现的,该标准通常也被称为“浮点数”。大部分十进制转二进制时是无限的,而计算机有限的空间无法存储无限的小数,只能存储一部分,丢失的那部分就造成了微差。

在计算如 0.1 + 0.2 结果是 0.30000000000000004 而非 0.3,这种错误并非 ECMAScript 所独有。其他使用相同格式的语言也有这个问题。‌

不同于“浮点数”的二进制存储机制,decimal.js 使用自定义的数字存储格式,Decimal实例通过三个属性精确表示十进制数:

  • d: 有效数字数组,存储十进制数的每一位数字
  • e: 指数部分
  • s: 符号部分, 1 表示正数, -1 表示负数

Decimal实例

使用示例

1
2
3
4
5
6
7
8
9
const Decimal = require("./decimal.js");

// 正常浮点数计算
console.log(0.1 + 0.2); // 0.30000000000000004

// decimal.js 计算
const a = new Decimal(0.1);
const b = new Decimal(0.2);
console.log(a.add(b).toString()); // 0.3

Decimal入口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Decimal.js

;(function (globalScope) {
'use strict';
...

// 创建并配置初始Decimal构造函数.
Decimal = clone(DEFAULTS);
Decimal.prototype.constructor = Decimal;
Decimal['default'] = Decimal.Decimal = Decimal;

...

// AMD实现.
if (typeof define == 'function' && define.amd) {
define(function () {
return Decimal;
});
// CMD实现.
} else if (typeof module != 'undefined' && module.exports) {
if (typeof Symbol == 'function' && typeof Symbol.iterator == 'symbol') {
P[Symbol['for']('nodejs.util.inspect.custom')] = P.toString;
P[Symbol.toStringTag] = 'Decimal';
}
module.exports = Decimal;
// 浏览器实现.
} else {
if (!globalScope) {
globalScope = typeof self != 'undefined' && self && self.self == self ? self : window;
}

noConflict = globalScope.Decimal;
Decimal.noConflict = function () {
globalScope.Decimal = noConflict;
return Decimal;
};

globalScope.Decimal = Decimal;
}

})(this);

DEFAULTS 默认配置

DEFAULTS 对象存储了 Decimal 构造函数的初始配置属性。这些属性定义了该库进行数值计算时的全局默认行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// Decimal 构造函数的初始配置属性
DEFAULTS = {

// 这些值必须是指定范围(包含边界值)内的整数.
// 大多数值可以在运行时通过 `Decimal.config` 方法进行更改.

// 计算或进制转换结果的最大有效数字位数。默认值为 20。
precision: 20, // 1 到 MAX_DIGITS

// 舍入到指定 `precision`(精度)时使用的舍入模式.
//
// ROUND_UP 0 远离零
// ROUND_DOWN 1 趋向零
// ROUND_CEIL 2 趋向 +∞
// ROUND_FLOOR 3 趋向 -∞
// ROUND_HALF_UP 4 四舍五入,.5 向上
// ROUND_HALF_DOWN 5 四舍五入,.5 向下
// ROUND_HALF_EVEN 6 四舍五入,.5 向偶数
// ROUND_HALF_CEIL 7 四舍五入,.5 向 +∞
// ROUND_HALF_FLOOR 8 四舍五入,.5 向 -∞

// 舍入模式.当结果需要截断到 precision 指定的精度时使用四舍五入,.5 向上舍入模式
// `Decimal.rounding = 4;`
// `Decimal.rounding = Decimal.ROUND_HALF_UP;`
rounding: 4, // 0 to 8

// 当计算模运算(a mod n)时使用的舍入模式。
// 商(q = a / n)根据对应的舍入模式进行计算。
// 余数(r)根据公式计算: r = a - n * q.
//
// UP 0 如果被除数为负数,则余数为正;否则为负数.
// DOWN 1 余数与被除数符号相同 (JavaScript %).
// FLOOR 3 余数与除数符号相同 (Python %).
// HALF_EVEN 6 IEEE 754 取余函数.
// EUCLID 9 欧几里得除法。q = sign(n) * floor(a / abs(n))。始终为正.
//
modulo: 1, // 0 to 9

// `toString` 返回指数表示法的最小指数值
toExpNeg: -7, // 0 to -EXP_LIMIT

// `toString` 返回指数表示法的指数值阈值
toExpPos: 21, // 0 to EXP_LIMIT

// 最小指数值,低于此值将发生下溢至零
// JavaScript numbers: -324 (5e-324)
minE: -EXP_LIMIT, // -1 to -EXP_LIMIT

// 最大指数值,高于此值将发生溢出至无穷大
// JavaScript numbers: 308 (1.7976931348623157e+308)
maxE: EXP_LIMIT, // 1 to EXP_LIMIT

// 是否使用加密安全的随机数生成器(如果可用)
crypto: false // true/false
},

rounding 舍入模式

值 名称 描述 对正数(1.5) 对负数(-1.5) 对正数(1.4) 对负数(-1.4)
0 ROUND_UP 远离零 2 -2 2 -2
1 ROUND_DOWN 趋向零 1 -1 1 -1
2 ROUND_CEIL 趋向 +∞ 2 -1 2 -1
3 ROUND_FLOOR 趋向 -∞ 1 -2 1 -2
4 ROUND_HALF_UP 四舍五入,.5 向上 2 -2 1 -1
5 ROUND_HALF_DOWN 四舍五入,.5 向下 1 -1 1 -1
6 ROUND_HALF_EVEN 四舍五入,.5 向偶数 2 -2 1 -1
7 ROUND_HALF_CEIL 四舍五入,.5 向 +∞ 2 -1 1 -1
8 ROUND_HALF_FLOOR 四舍五入,.5 向 -∞ 1 -2 1 -1

clone函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/*
* 创建并返回一个 Decimal 构造函数,其配置属性与此 Decimal 构造函数相同
*/
function clone(obj) {
var i, p, ps;

/**
* Decimal 构造函数
* 返回一个新的 Decimal 实例
*/
function Decimal(v) {
...
}

// 为 Decimal 构造函数设置自定义的 toStringTag 属性,提高代码的可读性和类型安全性.
// 定义前:(new Decimal(1.23)).toString() === '[object Object]'
// 定义后:(new Decimal(1.23)).toString() === '[object Decimal]'
tag = '[object Decimal]',
P = { toStringTag: tag };
Decimal.prototype = P;


// 定义 Decimal 构造函数的属性,用于配置 Decimal 实例的行为
Decimal.ROUND_UP = 0;
Decimal.ROUND_DOWN = 1;
Decimal.ROUND_CEIL = 2;
Decimal.ROUND_FLOOR = 3;
Decimal.ROUND_HALF_UP = 4;
Decimal.ROUND_HALF_DOWN = 5;
Decimal.ROUND_HALF_EVEN = 6;
Decimal.ROUND_HALF_CEIL = 7;
Decimal.ROUND_HALF_FLOOR = 8;
Decimal.EUCLID = 9;

Decimal.config = Decimal.set = config;
Decimal.clone = clone;
Decimal.isDecimal = isDecimalInstance;

Decimal.abs = abs;
Decimal.acos = acos;
Decimal.acosh = acosh; // ES6
Decimal.add = add;
Decimal.asin = asin;
Decimal.asinh = asinh; // ES6
Decimal.atan = atan;
Decimal.atanh = atanh; // ES6
Decimal.atan2 = atan2;
Decimal.cbrt = cbrt; // ES6
Decimal.ceil = ceil;
Decimal.clamp = clamp;
Decimal.cos = cos;
Decimal.cosh = cosh; // ES6
Decimal.div = div;
Decimal.exp = exp;
Decimal.floor = floor;
Decimal.hypot = hypot; // ES6
Decimal.ln = ln;
Decimal.log = log;
Decimal.log10 = log10; // ES6
Decimal.log2 = log2; // ES6
Decimal.max = max;
Decimal.min = min;
Decimal.mod = mod;
Decimal.mul = mul;
Decimal.pow = pow;
Decimal.random = random;
Decimal.round = round;
Decimal.sign = sign; // ES6
Decimal.sin = sin;
Decimal.sinh = sinh; // ES6
Decimal.sqrt = sqrt;
Decimal.sub = sub;
Decimal.sum = sum;
Decimal.tan = tan;
Decimal.tanh = tanh; // ES6
Decimal.trunc = trunc; // ES6

// 1. 如果 obj 是 undefined,初始化为空对象
if (obj === void 0) obj = {};
if (obj) {
// 如果 obj.defaults 不是 true
if (obj.defaults !== true) {
ps = ['precision', 'rounding', 'toExpNeg', 'toExpPos', 'maxE', 'minE', 'modulo', 'crypto'];
// 遍历 ps 数组,将 obj 中不存在的属性从当前实例(this)中拷贝到 obj
for (i = 0; i < ps.length;) if (!obj.hasOwnProperty(p = ps[i++])) obj[p] = this[p];
}
}

Decimal.config(obj);

return Decimal;
}

Decimal构造函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/**
* @param {*} v 初始化值,可以是数字、字符串、BigInt或Decimal实例.
* @returns {Decimal}
*/
function Decimal(v) {
var e, // 指数位变量,用来表示次方数
i, // 用来计算指数位的临时变量
t, // 参数类型临时变量
x = this;

// 如果没有使用 new 关键字调用构造函数,返回一个新的 Decimal 实例.
if (!(x instanceof Decimal)) return new Decimal(v);

// 保留对这个Decimal构造函数的引用,并覆盖Decimal.prototype.constructor,该属性原本指向Object
x.constructor = Decimal;

// 如果 v 是一个 Decimal 实例,直接计算 s、e、d的值.
if (isDecimalInstance(v)) {
x.s = v.s;

if (external) {
if (!v.d || v.e > Decimal.maxE) {
// Infinity.
x.e = NaN;
x.d = null;
} else if (v.e < Decimal.minE) {
// Zero.
x.e = 0;
x.d = [0];
} else {
x.e = v.e;
x.d = v.d.slice();
}
} else {
x.e = v.e;
x.d = v.d ? v.d.slice() : v.d;
}

return;
}

// 判断传入的参数类型
t = typeof v;

// 如果 v 是一个数字,直接计算 s、e、d的值.
if (t === "number") {
// 处理零
if (v === 0) {
// 处理零的 s 值,区分正负零
x.s = 1 / v < 0 ? -1 : 1;
x.e = 0;
x.d = [0];
return;
}
// 处理负数
if (v < 0) {
// 将负数转换为正数
v = -v;
// 标记为负数
x.s = -1;
} else {
// 标记为正数
x.s = 1;
}

// 处理整数且 v < 1e7
if (v === ~~v && v < 1e7) {
// 计算指数位
for (e = 0, i = v; i >= 10; i /= 10) e++;

if (external) {
if (e > Decimal.maxE) {
// 处理指数位过大的情况
x.e = NaN;
x.d = null;
} else if (e < Decimal.minE) {
// 处理指数位过小的情况
x.e = 0;
x.d = [0];
} else {
// 处理指数位在范围内的情况
x.e = e;
x.d = [v];
}
} else {
x.e = e;
x.d = [v];
}

return;
}

// 处理无穷大和 NaN
if (v * 0 !== 0) {
if (!v) x.s = NaN;
x.e = NaN;
x.d = null;
return;
}

return parseDecimal(x, v.toString());
}

// 如果 v 是一个字符串
if (t === "string") {
// 判断字符串的‌第一个字符是否为减号(-)
if ((i = v.charCodeAt(0)) === 45) {
// 移除负号(-)
v = v.slice(1);
// 标记为负数
x.s = -1;
} else {
// 判断字符串的‌第一个字符是否为加号(+), 移除加号(+)
if (i === 43) v = v.slice(1);
// 标记为正数
x.s = 1;
}

// 判断字符串是否为十进制数
return isDecimal.test(v) ? parseDecimal(x, v) : parseOther(x, v);
}

// 如果 v 是一个 BigInt
if (t === "bigint") {
// 处理负数
if (v < 0) {
// 将负数转换为正数
v = -v;
// 标记为负数
x.s = -1;
} else {
// 标记为正数
x.s = 1;
}
return parseDecimal(x, v.toString());
}

// 如果 v 是其他类型,抛出错误
throw Error(invalidArgument + v);
}

parseDecimal 函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
* 将字符串 str 解析为 Decimal 对象的内部表示
*/
function parseDecimal(x, str) {
// 声明三个局部变量
var e, i, len;

// e 赋值为小数点位置索引,如果存在小数点,将小数点替换为空字符串
if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');

// i 赋值为指数位位置索引,判断是否存在指数位
if ((i = str.search(/e/i)) > 0) {

// 如果之前没找到小数点(e < 0),则将 e 设为指数标志的位置 i
if (e < 0) e = i;
// 将 e 加上指数部分的值
e += +str.slice(i + 1);
// 截取指数标志之前的部分作为有效数字
str = str.substring(0, i);
} else if (e < 0) {
//如果字符串没有指数形式(i <= 0)且没有小数点(e < 0),说明是纯整数,指数 e 设为字符串长度
e = str.length;
}

// 确定前导零:从字符串开头开始,只要字符编码等于 48(字符 '0'),i 就递增。循环结束后,i 是第一个非零字符的位置
for (i = 0; str.charCodeAt(i) === 48; i++);

// 确定尾随零:从字符串末尾开始,只要最后一个字符是 '0',len 就递减。循环结束后,len 是最后一个非零字符位置的后一位
for (len = str.length; str.charCodeAt(len - 1) === 48; --len);
// 截取字符串,移除前导零和尾随零。此时 str 只包含有效数字部分
str = str.slice(i, len);

// 如果截取后字符串非空(即值不为零),进入处理分支
if (str) {
// 更新 len 为有效数字的长度
len -= i;
// 计算并存储指数值
x.e = e = e - i - 1;
// 初始化数字数组 d(digits),用于存储有效数字的字
x.d = [];

// 转换底数

// e 是以 10 为底的指数.
// 计算第一个数字字应该从哪里开始切分
i = (e + 1) % LOG_BASE;
// 如果 e < 0,需要加上 LOG_BASE 取模修正
if (e < 0) i += LOG_BASE;

// 分段处理字符串,按 LOG_BASE 位一组转换为数字存入 x.d 数组:
if (i < len) {
if (i) x.d.push(+str.slice(0, i));
for (len -= LOG_BASE; i < len;) x.d.push(+str.slice(i, i += LOG_BASE));
str = str.slice(i);
i = LOG_BASE - str.length;
} else {
i -= len;
}

// 补足尾随零,使最后一个字达到 LOG_BASE 位长度,然后将其转换为数字存入 x.d 数组。
for (; i--;) str += '0';
x.d.push(+str);

if (external) {

// 如果指数 x.e 大于 maxE,将值设为 Infinity(d = null, e = NaN)
if (x.e > x.constructor.maxE) {
// Infinity.
x.d = null;
x.e = NaN;

// 如果指数 x.e 小于 minE,将值设为 0(e = 0, d = [0])
} else if (x.e < x.constructor.minE) {
x.e = 0;
x.d = [0];
}
}
} else {

// Zero.
x.e = 0;
x.d = [0];
}

// 返回填充好的 Decimal 对象
return x;
}

add / plus 函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
P.plus = P.add = function (y) {
var carry, d, e, i, k, len, pr, rm, xd, yd,
// x赋值为调用的 Decimal 对象,
x = this,
// Ctor赋值为 x 的构造函数(Decimal)
Ctor = x.constructor;
// 将参数 y 转换为 Decimal 对象
y = new Ctor(y);

// d 为 null 表示 Infinity,d 为 undefined 或特定值表示 NaN,检查是否有操作数不是有限数
if (!x.d || !y.d) {

// 如果任一操作数是 NaN(s 为 null 或 undefined),返回 NaN
if (!x.s || !y.s) y = new Ctor(NaN);

// 如果 x 是 Infinity(!x.d),y 是有限的,返回 x
// 如果两者都是 Infinity 且符号相同,返回 x
// 如果两者都是 Infinity 但符号不同,返回 NaN
// 如果 x 有限,y 是 Infinity,返回 y
else if (!x.d) y = new Ctor(y.d || x.s === y.s ? x : NaN);

return y;
}

// 如果两个数的符号不同,将 y 的符号取反,然后调用减法 minus 方法。
if (x.s != y.s) {
y.s = -y.s;
return x.minus(y);
}

xd = x.d; // 获取 x 的数字数组
yd = y.d; // 获取 y 的数字数组
pr = Ctor.precision; // 获取Decimal 值的有效数字位数
rm = Ctor.rounding; // 获取四舍五入规则

// 零值处理
if (!xd[0] || !yd[0]) {

// 如果 y 为零,返回 x 的副本
// 如果 y 非零(说明 x 为零),返回 y
if (!yd[0]) y = new Ctor(x);

// 最后根据是否 external 调用 finalise 进行舍入
return external ? finalise(y, pr, rm) : y;
}

// x 和 y 都是有限的、非零的、同号的数

// 计算基数指数 k 和 e
// k 是 x 的指数,e 是 y 的指数
// 计算差值 i = k - e
// 如果 i 小于 0,说明 x 的指数大于 y 的指数,需要将 xd 数组的数字左移 i 位
// 如果 i 大于 0,说明 y 的指数大于 x 的指数,需要将 yd 数组的数字左移 i 位
k = mathfloor(x.e / LOG_BASE);
e = mathfloor(y.e / LOG_BASE);

// 复制 xd 数组,避免直接修改原数组
xd = xd.slice();
// 计算基数差值 i = k - e
i = k - e;

// 如果指数不同,需要对齐小数点位置
if (i) {
// 如果 i < 0:x 的指数小于 y 的指数,需要调整 xd(较小数)
if (i < 0) {
// 记录被调整的数组 d 和另一个数组的长度 len
d = xd;
i = -i;
len = yd.length;
} else {
// 如果 i > 0:x 的指数大于 y 的指数,需要调整 yd
d = yd;
e = k;
len = xd.length;
}

// 计算需要前置零的最大数量,以保证精度足够。
k = Math.ceil(pr / LOG_BASE);
len = k > len ? k + 1 : len + 1;

// 如果指数差 i 超过 len,限制 i 为 len,并将数组长度设为 1(后续会补零)
if (i > len) {
i = len;
d.length = 1;
}

// 对齐指数:通过反转数组,在末尾添加零,再反转回来,实现前置零的效果。
// 这比 unshift 更高效
d.reverse();
for (; i--;) d.push(0);
d.reverse();
}

// 获取 xd 和 yd 的长度 len 和 i
len = xd.length;
i = yd.length;

// 如果 yd 数组更长,交换 xd 和 yd 数组,使 xd 指向更长的数组
if (len - i < 0) {
i = len;
d = yd;
yd = xd;
xd = d;
}

// 从低位到高位逐字相加,只循环 yd.length 次,因为 xd 更高的位无需修改
for (carry = 0; i;) {
// carry 存储进位,每个字相加后除以 BASE(10^7)得到进位
carry = (xd[--i] = xd[i] + yd[i] + carry) / BASE | 0;
// 当前位取模 BASE 得到结果
xd[i] %= BASE;
}

// 如果最后有进位,在数组最前面插入进位,并增加指数
if (carry) {
xd.unshift(carry);
++e;
}

// 移除尾随零
for (len = xd.length; xd[--len] == 0;) xd.pop();

// 将结果存储到 y 对象
y.d = xd;
// 计算十进制指数
y.e = getBase10Exponent(xd, e);

// 返回结果
return external ? finalise(y, pr, rm) : y;
};

Decimal.js计算逻辑
https://www.my-web.cn/js/decimal/
发布于
2026年6月11日
许可协议