低代码公式配置数字计算精度处理/大数计算处理记录

在低代码平台中,会涉及到用户自定义一个公式,该公式的运算结果即为字段的数据,如下:

很明显的用户配置了一个加法的逻辑,工资的数值大多数的情况下是有小数的,并且由于这个公式是用户配置使用,用来计算的数据的小数位数也是用户决定的。由于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 采用了以下解决方案:

  1. 设计 Op 类封装数值操作
  2. Babel 插件转换 + - * / 运算符为 add, sub, mul, div 方法
  3. 实现各种操作符的重载方法(如 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...

相关推荐
Moment1 小时前
从方案到原理,带你从零到一实现一个 前端白屏 检测的 SDK ☺️☺️☺️
前端·javascript·面试
野生的程序媛2 小时前
重生之我在学Vue--第5天 Vue 3 路由管理(Vue Router)
前端·javascript·vue.js
鱼樱前端2 小时前
Vue 2 与 Vue 3 响应式原理详细对比
javascript·vue.js
codingandsleeping2 小时前
前端工程化之模块化
前端·javascript
CodeCraft Studio2 小时前
报表控件stimulsoft操作:使用 Angular 应用程序的报告查看器组件
前端·javascript·angular.js
Liigo3 小时前
初次体验Tauri和Sycamore(3)通道实现
javascript·rust·electron·tauri·channel·sycamore
烛阴3 小时前
JavaScript 性能提升秘籍:WeakMap 和 WeakSet 你用对了吗?
前端·javascript
专注VB编程开发20年4 小时前
JS采集数据爬虫-Fetch API 和 XMLHttpRequest 有什么区别?
开发语言·javascript·爬虫·js
鱼樱前端4 小时前
Vue 2 与 Vue 3 语法区别完整对比
前端·javascript·vue.js
萌萌哒草头将军4 小时前
🚀🚀🚀 服务器防吃灰指南(二) !
java·服务器·javascript