只有前端会数字运算不准确吗?后端为什么不会这样?实际上并非只有前端的 javascript 有数字计算的精度问题,其他的常见语言默认都有这个问题,大家可以自己去试一试,只不过其他的语言都自带了精准计算的库,所以后端在处理数字的时候会使用语言自带的模块来保证数字计算的精准,而 javascript 目前还没有自带相关模块,只有第三方的模块,不过未来的 javascript 是可能自带精准计算模块的,这里有一个相关提案,目前处于 Stage 1 阶段,目前提案的语法如下:
function calculateBill(items, tax) { let total = 0m; for (let {price, count} of items) { total += price * BigDecimal(count); } return BigDecimal.round( total * (1m + tax), {maximumFractionDigits: 2, round: "up"} ); } let items = [{price: 1.25m, count: 5}, {price: 5m, count: 1}]; let tax = .0735m; console.log(calculateBill(items, tax));
当然上面的代码现在是无法使用的,只是提案的一个语法展示例子,而且即便支持了这个语法,从易用性上来说也不如我们接下来介绍的方法。
怎样完美解决数字计算精度问题和数字格式化问题?
这里我个人推荐 a-calc
库,这个库具备精准计算需求也兼顾了数字格式化和易用性的需求,我们都知道 bignumber.js 这类库最大的问题就是操作数一旦多起来那么就非常不直观,我们来看看 a-calc
是怎么使用的。
基础的运算:
import { calc } from "a-calc" calc("0.1 + 0.2") // 0.3
变量运算:
calc(" (a + b) / 2 ", {a: 2, b: 6}) // 4
你没看错就是这么简单,编写方式非常符合直觉,接下来我们再来看看一个复杂一点的计算式。
calc("1 + o.a / arr[0].d",{ o: { a: 2 }, arr: [{ d: 8 }] }) // 1.25
到这里你可能有一个疑问,如果变量没取到,或者因为某些原因导致计算式非法怎么办?默认当然是报错了,不过贴心的 a-calc
自然也给了你更方便的功能,错误返回默认值。
calc(" a + 1 ", {a: undefined, _error: 0}) // 0
上面的 a 是 undefined 那么计算式就不会成立,无法计算,但是一旦你传入了 _error
参数,在即将报错的时候他会返回给你 0
,这样你就不用自己做错误处理了,慢着,这就完了吗?当然不是,都说了要完美了,这些功能自然还不够!接下来我们说说数字格式化。
我们实际开发中可能在计算完成之后做常规的数字格式化或者就单纯需要数字格式化,例如千分位,保留小数位,转换成百分比,禁止输出科学计数法,转换成分数,等等,放心,这些全部支持,而且更强大 a-calc
除了支持上面的格式化还支持动态保留小数位,保留正负号,带单位计算等等,下面直接上代码。
// 注意格式化部分使用 | 与表达式部分分隔开,写在右边即可 calc("1 + 1 | =2") // 2.00 =2 的意思就是小数点位数保留 2 位,既然有等于那么你也应该想到了,大于 小于 大于等于和小于等于也是支持的 calc("132424232423423 + 2132243242 | ,") // 132,426,364,666,665 这里一个逗号就表示千分位了 calc("0.025 + 0.2 | /") // 9/40 分数输出也很直观!到这里百分比输出还用演示吗?想必大家应该已经知道了就是一个百分号就行了。 calc("1 + 2%", {_unit: true}) // 3% 带单位计算依然不在话下
良好的 typescript 支持
如果你使用了 typescript,那么依然可以等到良好的 ts 类型提醒,正常情况下你可以使用 calc
一把梭哈,但是 a-calc
依然提供了一个更加强大智能的 calc_wrap
函数:
// 注意这里将 calc_wrap 重命名为 calc, 因为如果你需要使用 calc_wrap 函数的时候,基本用不到核心的 calc 函数,那么有这个闲置好名字就应该拿来用 import { calc_wrap as calc } from "a-calc"; const state = { a: 1, b: 2, c: 3 }; // 当传入的参数是一个不含变量名的计算式将会直接返回计算结果 calc( "(1 + 2) * 3" ); // 返回类型: string // 当传入的参数是一个疑似包含变量名的计算式且没有第二个数据源参数时,会返回一个等待传入数据源的函数,没错这个功能通过静态类型的推导做到了 calc( "(a + b) * c" ); // 返回类型: ( data: any ) => string calc( "(a + b) * c" )( state ); // 返回类型: string // 也许你希望先注入状态然后在输入表达式,这也是可以的 calc( state ); // 返回类型: ( expr: string | number ) => string calc( state )( "(a + b) * c" ); // 返回类型: string // 原本的用法自然也是支持的 calc( "a + b + c", state ); // 返回类型: string // 你依然可以将配置与数据源混合在一起,这是非常方便的 calc( "a + b + c" )( { ...state, _error: 0 } ); // 返回类型: string | 0
好了到这里你应该也了解了 a-calc
大部分的使用方式,更具体的内容可以查看官方文档:官方文档链接