JS toFixed的坑以及四舍五入实现方法
toFixed() 方法将数值转换为一个字符串,该字符串表示该数值与指定小数位数的最近近似值。当需要四舍五入时,它遵循IEEE 754标准中的四舍五入规则,这与一般的数学四舍五入规则有所不同。
语法:
number.toFixed(digits)
digits:指定小数位数。
toFixed() 会将数字四舍五入到指定的小数位数,并返回一个字符串。如果小数位数不足,会用 0 填充。
toFixed() 遵循银行家舍入法,但有些时候和好像和银行家舍入法的结果不同,其实这是我们误会 toFixed() 了,主要是因为 浮点数在用二进制中存储时有精度误差导致 toFixed() 用银行家四舍五入时不准。
银行家舍入法
我国金融系统的大部分算法就是用四舍五入,国际上欧盟委员会对换汇时的舍入规定也是我们常见的四舍五入。
真正广泛采用银行家舍入法的,是需要更小误差的科学和计算机系统,因为多次舍入时银行家舍入法会比传统舍入法误差更小,因此银行家舍入法也叫统计学家舍入(statistician's rounding)、无偏舍入(unbiased rounding)。现在大部分编程软件的默认舍入都是银行家舍入法,比如 c/c++、javascript、php、go,英特尔处理器用的也是银行家舍入。
规则四舍六入五取偶:
•(1)被修约的数字小于5时,该数字舍去;
•(2)被修约的数字大于5时,则进位;
•(3)被修约的数字等于5时,要看5前面的数字,若是奇数则进位,若是偶数则将5舍掉,即修约后末尾数字都成为偶数;若5的后面还有不为“0”的任何数,则此时无论5的前面是奇数还是偶数,均应进位。
示例:
•9.8249=9.82,9.82671=9.83
•9.835=9.84,9.8351 =9.84
•9.825=9.82,9.82501=9.83
简单的说,就是:四舍六入五考虑,五后非空就进一,五后为空看奇偶,五前为偶应舍去,五前为奇要进一
测试银行家舍入法正常情况:
// 5 后非 0 进位后舍去
const err1 = 22.4451
console.log('err1', err1.toFixed(2)); // 22.45
// 5 后非 0 进位后舍去
const err1 = 22.445002
console.log('err1', err1.toFixed(2)); // 22.45
// 5后无,5前一位为奇数 进位
const err1 = 22.475
console.log('err1', err1.toFixed(2)); // 22.48
// 5后无,5前一位为偶数 舍去
const err1 = 22.485
console.log('err1', err1.toFixed(2)); // 22.48
测试异常情况:
// 5后无,5前一位为奇数 进位
const err1 = 22.415
console.log('err1', err1.toFixed(2)); // 22.41
// 5后无,5前一位为奇数 进位
const err1 = 22.455
console.log('err1', err1.toFixed(2)); // 22.45
// 5后无,5前一位为偶数 舍去
const err1 = 22.445
console.log('err1', err1.toFixed(2)); // 22.45
// 5后无,5前一位为偶数 舍去
const err1 = 22.425
console.log('err1', err1.toFixed(2)); // 22.43
// 5后无,5前一位为偶数 舍去
const err1 = 22.405
console.log('err1', err1.toFixed(2)); // 22.41
// 5后无,5前一位为奇数 进位
const err1 = 0.015
console.log('err1', err1.toFixed(2)); // 0.01
注意:这个挺吓人的,0.015 变成 0.01 慎用啊
原因分析:
const err1 = 1.005
console.log('err1', err1.toFixed(2)); // "1.00"(预期是 "1.01")
原因:浮点数在内存中的实际值可能略小于表面值。例如,`1.005` 实际存储为 `1.00499999999999989341858963598497211933135986328125`,导致四舍五入错误。
可以用下面方法查询浮点数在内存中存的值 getDecimalValue
function getDecimalValue(floatNum) {// 这里不用 Decimal.toBinary, 因为toString 更准确const binaryString = '0b' + floatNum.toString(2) // 将小数转为二进制// console.log(binaryString)let decimalValue = new Decimal(binaryString, 2).toString(); // 直接从二进制转换并创建Decimal对象console.log(decimalValue);return Number(decimalValue);}
然后重新测一下上面的值发现结果都遵循银行家舍入了。
const err1 = 22.415
// 22.41499999999999914734871708787977695465087890625
console.log(getDecimalValue(err1).toFixed(2)); // 22.41
const err1 = 22.455
// 22.4549999999999982946974341757595539093017578125
console.log(getDecimalValue(err1).toFixed(2)); // 22.45
const err1 = 22.445
// 22.44500000000000028421709430404007434844970703125
console.log(getDecimalValue(err1).toFixed(2)); // 22.45
const err1 = 22.425
// 22.425000000000000710542735760100185871124267578125
console.log(getDecimalValue(err1).toFixed(2)); // 22.43
const err1 = 22.425
// 22.405000000000001136868377216160297393798828125
console.log(getDecimalValue(err1).toFixed(2)); // 22.41
const err1 = 0.015
// 0.01499999999999999944488848768742172978818416595458984375
console.log(getDecimalValue(err1).toFixed(2)); // 0.01
但是浮点数有数度问题怎么办呢,本人觉得可以考虑其它方法,自己写一个方法或是用下面方法
使用正常四舍五入
看到网上有很多这么写的,其实是不对的,不加 toFixed,这个结果是 1,没有 .00
function roundUp(num, digits) {const factor = 10 ** digits;return (Math.round(num * factor) / factor).toFixed(digits);}const test = 1.005console.log(roundUp(test, 2));
上面结果还是 1.00 ,不是 1.01,还是有精度问题
因为 1.005 存储在内存中值是 1.00499999999999989341858963598497211933135986328125
乘以 100 后是 100.499999........ 四舍五入后还是 100 而不是 101,所以还是使用高精度库函数
也有用下面这种方法的,但是用 23.005 保留 2 位小数, 测试就不准了
function roundUp(num, digits) {const factor = 10 ** digitsconst roundAdd1 = Math.round(num * factor * 10)const round = Math.round(roundAdd1 / 10)return (round / factor).toFixed(digits)}
Big.js
function roundUp(num, precision = 0) {const bigNum = new Big(num).round(precision)return bigNum.toNumber().valueOf()}const test = 1.005console.log(new Big(test).toNumber().valueOf()); // 1.005console.log(roundUp(test, 2)); // 1.01
decimal.js
查看了源码,decimal.js 的 toFixed 默认用的是正常四舍五入
const num3 = 1.005;console.log(new Decimal(num3).toFixed(2).toString());
如果需要配置银行家舍入法,可以进行设置
Decimal.set({ rounding: 5 }) 或是 const Decimal9 = Decimal.clone({ rounding: 5 }) b = new Decimal9(1)
rounding 配置表,默认是 1 (ROUND_DOWN)
Property | Value | Description |
---|---|---|
ROUND_UP | 0 | Rounds away from zero |
ROUND_DOWN | 1 | Rounds towards zero |
ROUND_CEIL | 2 | Rounds towards Infinity |
ROUND_FLOOR | 3 | Rounds towards -Infinity |
ROUND_HALF_UP | 4 | Rounds towards nearest neighbour. If equidistant, rounds away from zero |
ROUND_HALF_DOWN | 5 | Rounds towards nearest neighbour. If equidistant, rounds towards zero |
ROUND_HALF_EVEN | 6 | Rounds towards nearest neighbour. If equidistant, rounds towards even neighbour |
ROUND_HALF_CEIL | 7 | Rounds towards nearest neighbour. If equidistant, rounds towards Infinity |
ROUND_HALF_FLOOR | 8 | Rounds towards nearest neighbour. If equidistant, rounds towards -Infinity |
EUCLID | 9 | Not a rounding mode, see modulo |
但是小数点后面位数过多时,高精度库存储也会有问题,这个可能是 JavaScript 使用64位双精度,由0或1组成64位,有效数位数只有52位,有效数部分无法存储无限长度的二进制,后面的数据就会丢失,计算机只能存储一个近似的数字。
尽管 Decimal.js
旨在提供比原生 JavaScript 数字类型更高的精度,但它仍然受到底层浮点数系统的限制。
const num3 = 1234567890.012345678;console.log(new Decimal(num3).toString()); // 1234567890.0123458