前言
最近在做一个ERP的项目,里面涉及到了很多的计算,尤其特别是有很多关于浮点数的计算,然后就碰到了下面的问题。
问题描述 & 解决方案
1.使用toFixed
or Math.round
进行四舍五入
&保留两位小数
会有5
不进1
的情况
举个🌰,我在开发的过程中遇到了321201.595
这个数字...
然后我想对他进行四舍五入 & 保留两位小数,一开始不太了解 toFixed
有那么多坑,所以直接用的.toFixed(2)
,结果如下:
const number = 321201.595; console.log(number.toFixed(321201.595)) // '321201.59' 复制代码
没错,结果不对,少了那么0.01
,后端算出来是321201.60
,所以校验不通过,我得再处理,网上翻了很多资料,都说用Math.round
可以做到四舍五入 & 保留两位小数,是这么做的,结果如下:
const number = 321201.595; console.log(Math.round(number * 100) / 100) // 321201.59 复制代码
没错,结果还是不对!!!WTF!!
这里说一下toFixed
& Math.round
toFixed 😭
toFixed()
方法可把 Number 四舍五入为指定小数位数的数字。例如将数据Num保留2位小数,则表示为:toFixed(Num);但是其四舍五入的规则与数学中的规则不同,使用的是银行家舍入规则
,银行家舍入:所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法
。
网上是这么说的:五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一
。
但是我跑了一遍发现,好像不对啊,5前为奇数的时候,只有321201.575; 321201.515
进1
了,相反5前面为偶数的时候除了321201.545
其他的都会进1。
可以证实的是,如果5后面还有数值,那就会直接进1
,参考321201.5951
继续用上面的321201.595
举个🌰:
console.log(321201.595.toFixed(2)) // '321201.59' console.log(321201.585.toFixed(2)) // '321201.59' console.log(321201.575.toFixed(2)) // '321201.58' console.log(321201.565.toFixed(2)) // '321201.57' console.log(321201.555.toFixed(2)) // '321201.55' console.log(321201.545.toFixed(2)) // '321201.54' console.log(321201.535.toFixed(2)) // '321201.53' console.log(321201.525.toFixed(2)) // '321201.53' console.log(321201.515.toFixed(2)) // '321201.52' console.log(321201.505.toFixed(2)) // '321201.51' console.log(321201.5951.toFixed(2)) // '321201.60' 复制代码
上面的数据显示:321201.595
& 321201.555
& 321201.545
& 321201.535
没有进1
,其他的都进了......
Math.round
网上说这个比较准确,round() 方法可把一个数字舍入为最接近的整数,我试了一下,也还是不准,举个🌰
console.log(Math.round(321201.595 * 100) / 100) // 321201.59 console.log(Math.round(321201.585 * 100) / 100) // 321201.59 console.log(Math.round(321201.575 * 100) / 100) // 321201.58 console.log(Math.round(321201.565 * 100) / 100) // 321201.57 console.log(Math.round(321201.555 * 100) / 100) // 321201.56 console.log(Math.round(321201.545 * 100) / 100) // 321201.55 console.log(Math.round(321201.535 * 100) / 100) // 321201.53 console.log(Math.round(321201.525 * 100) / 100) // 321201.53 console.log(Math.round(321201.515 * 100) / 100) // 321201.52 console.log(Math.round(321201.505 * 100) / 100) // 321201.51 console.log(Math.round(321201.5351 * 100) / 100) // 321201.6 console.log(Math.round(321201.5951 * 100) / 100) // 321201.6 复制代码
上面的数据显示,321201.595
& 321201.535
没有进1
,其他的都进了......
这是因为...321201.595 * 100
!== 32120159.5
,而是32120159.499999996
,这个问题是计算精度的问题;
解决
既然数字靠不住,那就处理字符串,因为项目的产品设计里只需要进行四舍五入保留两位数
,所以为了快速修复问题,就针对两位数进行处理:
function num2Fixed(num: number) { const numStr = num.toString(); if (numStr.includes('.')) { const numArr = numStr.split('.'); const decimalNum = parseInt(`${numArr[1][2]}`, 10); let numcArr = []; if (decimalNum >= 5) { numcArr = (num + 0.01).toString().split('.'); // 这里不放心的话可以用mathjs的方法 } else { numcArr = num.toString().split('.'); } return parseFloat(`${numcArr[0]}.${numcArr[1].substring(0, 2)}`); } return num; } 复制代码
思路就是,把数字转成字符串,处理小数点后的第三位,如果大于等于5,就在原来的基础上 + 0.01
,目前来看没有什么问题。
- 两个浮点数做乘法,精度丢失的情况
这个问题是在是无从下手,因为涉及到了加减乘除,无法用字符串再进行操作,找了一圈,还是选择用mathjs
来解决(内心OS: 真不想用,用了它还得去解决打包依赖的问题,Vite我还不是很熟),具体操作如下:
import * as math from 'mathjs'; // 这里需要注意,新版的mathjs修改了import的方式 const { create, all } = math; const $math = create(all, { epsilon: 1e-12, matrix: 'Matrix', number: 'BigNumber', // 可选值:number BigNumber precision: 64, predictable: false, randomSeed: null }); /** Js 精度计算的方法 */ function mathComputed(evalstr: string, need2fixed = true) { const num = Number($math.format($math.evaluate(evalstr))); if (need2fixed) { // 是否需要进行 四舍五入,保留两位小数的处理 return num2Fixed(num); } return num; } 复制代码
以上就是我遇到的问题,希望大家以后可以有效避开这些错误(谁项目里用了toFixed了,赶紧去检查一下吧!)