在低代码平台中,会涉及到用户自定义一个公式,该公式的运算结果即为字段的数据,如下:
很明显的用户配置了一个加法的逻辑,工资的数值大多数的情况下是有小数的,并且由于这个公式是用户配置使用,用来计算的数据的小数位数也是用户决定的。由于JavaScript 中浮点数计算存在精度问题(采用的是双精度(64位)浮点运算规则),如果不处理会遇到如下问题:
javascript
0.1 + 0.2 // 结果为 0.30000000000000004 而不是预期的 0.3
这种精度问题在用户使用公式规则配置为字段的数据时,比如金额利息计算会出现。再者说对于大数计算,客户可能在业务中也会遇到,因为超出js的最大数值也会影响表单数据计算的正确性。用户配置规则的充满不确定性,那么解决这个隐形的精度问题就变得必要了
客户可不会管js底层的精度问题或超长问题,下面是解决的一个记录:
方案
toFixed 方法
最简单粗暴的是 toFixed 方法,对数据进行四舍五入,不足位用0补齐。但也存在一些结果不精准的问题:
javascript
const numObj = 12345.6789;
numObj.toFixed(1); // '12345.7';向上舍入
numObj.toFixed(6); // '12345.678900';用零补足位数
(2.34).toFixed(1); // '2.3'
(2.35).toFixed(1); // '2.4';向上舍入
(2.55).toFixed(1); // '2.5'
// 它向下舍入,因为它无法用浮点数精确表示,并且最接近的可表示浮点数较小
(2.449999999999999999).toFixed(1); // '2.5'
// 向上舍入,因为它与 2.45 的差值小于 Number.EPSILON
在一些单次简单计算、对精度要求不极高的场景下,toFixed 方法可以使用。很明显,对于公式规则不使用,用户的场景动态可配置,需要适配各种场景
整数化处理
原因是小数引起的,使用整数就不会出现这个精度问题了
比如下面这个加法的逻辑:
javascript
function multiplyFn(num1, num2, operation = "add") {
const decimalPlaces1 = (num1.toString().split(".")[1] || "").length;
const decimalPlaces2 = (num2.toString().split(".")[1] || "").length;
// 获取最大的小数位数
const maxDecimal = Math.max(decimalPlaces1, decimalPlaces2);
// 放大倍数
const multiplier = Math.pow(10, maxDecimal);
const int1 = Math.round(num1 * multiplier);
const int2 = Math.round(num2 * multiplier);
// 执行计算后再除回原精度
let result;
switch (operation) {
case "add":
result = (int1 + int2) / multiplier;
break;
case "subtract":
result = (int1 - int2) / multiplier;
break;
case "multiply":
result = (int1 * int2) / (multiplier * multiplier);
break;
case "divide":
result = int1 / int2;
break;
default:
throw new Error("不支持的操作符");
}
return result;
}
console.log(multiplyMethod(0.1, 0.2, 'add')); // 0.3
console.log(multiplyMethod(1.005, 0, 'add').toFixed(2)); // '1.01'
简单直观,非常的棒,很多的第三方库也是基于这样的思路来处理的,比如currency.js,适用于简单货币计算
可惜存在大数计算的问题,当Number超出JavaScript的最大安全整型值Number.MAX_VALUE
时,会出现精度损失
从ES2020开始,JavaScript引入了BigInt类型,专门用于表示任意精度的整数,可以使用它来表示比Number更大的整数,处理大数计算的问题
javascript
const bigNumber = 9007199254740991n; // 使用n后缀表示是BigInt类型
// 或者使用BigInt构造函数
const anotherBigNumber = BigInt("9007199254740991234567890");
// 基本运算
const sum = bigNumber + anotherBigNumber;
const value1 = bigNumber * 2n;
const value2 = anotherBigNumber / bigNumber;
console.log(sum); // 9007199254740991234567881n
console.log(value1); // 18014398509481982n
console.log(value2); // 1000000000n
// 注意:BigInt和Number不能直接混合运算
// 这会报错: const mixed = bigNumber + 10 ❌
这是一个不错的解决方式,但 BigInt 只支持整数运算,不能直接与浮点数进行运算
BigNumber.js
当前业务上需要的是高精度的且有大数处理,列入财务计算、货币计算、需要任意精度的场景。这样的功能在第三方库有成熟的解决方案,比如BigNumber.js、decimal.js、big.js
因为可能会涉及财务计算、金融运算,包的大小是8k,不会太大,api也满足计算需求,最终使用了BigNumber.js
它支持常见的数学运算,且可链式调用
javascript
0.1 + 0.2 // 0.30000000000000004
x = new BigNumber(0.1)
y = x.plus(0.2) // '0.3'
BigNumber(0.7).plus(x).plus(y) // '1'
x.plus('0.1', 8) // '0.225'
实现细节
如开头示例,配置出来的表达式大致是:
javascript
( componentValue.invoke('基本工资') + componentValue.invoke('绩效工资') ) * componentValue.invoke('公积金比例')
RuleEngine 采用了以下解决方案:
- 设计
Op
类封装数值操作 - Babel 插件转换 + - * / 运算符为
add
,sub
,mul
,div
方法 - 实现各种操作符的重载方法(如
add
,sub
等),在方法里面去调用bigNumber的计算方法
上面的表达式经过Babel插件后会被转换为:
javascript
Op.mul(
Op.add(
componentValue.invoke('基本工资'),
componentValue.invoke('绩效工资')
),
componentValue.invoke('公积金比例')
)
转换完成之后在通过new function 构建好执行函数即可调用方法获取结果。
下面是一些核心方法:
核心数值计算方法
javascript
computeNumber(a, b, type) {
let result = 0;
const bigNumberA = new BigNumber(a)
switch (type) {
case "+": result = bigNumberA.plus(b); break;
case "-": result = bigNumberA.minus(b); break;
case "*": result = bigNumberA.multipliedBy(b); break;
case "/": result = bigNumberA.div(b); break;
}
return result.valueOf()
}
加法运算实现
javascript
add(num1, num2) {
return this.computeNumber(num1, num2, '+');
}
最终效果
使用 BigNumber 库确保了 0.1 + 0.2 = 0.3
,在理论上公式规则支持的计算都是精准计算,避免了 JavaScript 浮点数计算的精度问题。
同时也为规则引擎提供了强大灵活的数值处理能力,确保在各种场景下都得到准确的计算结果
最近在看之前做的一些内容,让看过做过的不白干
希望能够和大家一起学习,一起成长,欢迎留言指点
参考:
大数计算:www.cnblogs.com/zhilin/p/17... bignumber.js 文档:juejin.cn/post/684490...