js 关于 toFixed 问题的总结,保留小数,四舍五入

js 关于 toFixed 问题的总结

https://blog.csdn.net/Leon_940002463/article/details/124094588


js 关于 toFixed 问题的总结
最近在工作中,在算一个处方费用医保结算的时候 遇到一个诡异的问题,会出现一分钱的差异,设计金额的计算 往往是医院和银行最关注的。经过一上午的排查,最后 发现竟然是完全信赖的 js 原生 toFixed 方法的问题。

test(){
console.log(1.05.toFixed(1)); // 1.1 对
console.log(1.005.toFixed(2)); // 1.00 错
console.log(1.0005.toFixed(3)); // 1.000 错
console.log(1.00005.toFixed(4)); // 1.0001 对
console.log(1.000005.toFixed(5)); // 1.00001 对
console.log(1.0000005.toFixed(6)); // 1.000001 对
console.log(1.00000005.toFixed(7)); // 1.0000000 错
console.log(1.000000005.toFixed(8)); // 1.00000000 错
}

看到上面的测试结果 我整个脑瓜子都是嗡嗡的

js封装解决问题

/**
 * 保留小数点几位数, 自动补零, 四舍五入
 * @param num: 数值
 * @param digit: 小数点后位数
 * @returns string
 * Object.is()是es6引入的、用于判断两个或者多个数据是否全等的方法。很重要的一个特点是Object.is(NaN,NaN)的结果是true  这里是判断这个值是否为数字 如果不为数字则 方法parseFloat 方法就不能转换 这里就会 显示为true
 * Number.EPSILON  可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。 这里是设置最小误差值 
 * Math.pow  原始方法 这里是求 10的多少次方
 */
function myFixed(num, digit) {
  if(Object.is(parseFloat(num), NaN)) {
    return console.log(`传入的值:${num}不是一个数字`);
  }
  num = parseFloat(num);
  return (Math.round((num + Number.EPSILON) * Math.pow(10, digit)) / Math.pow(10, digit)).toFixed(digit);
}


test(){
console.log(myFixed(1.05,1)); // 1.1 对
console.log(myFixed(1.005,2)); // 1.01 对
console.log(myFixed(1.0005,3)); // 1.001 对
console.log(myFixed(1.00005,4)); // 1.0001 对
console.log(myFixed(1.000005,5)); // 1.00001 对
console.log(myFixed(1.0000005,6)); // 1.000001 对
console.log(myFixed(1.00000005,7)); // 1.0000001 对
console.log(myFixed(1.000000005,8)); // 1.00000001 对
}

eg: 
var f = 3.15
undefined
f.toFixed(1)
'3.1'
myFixed(f,1)
'3.2'



好了问题解决了 ,现在我们追一下 tofixed 的具体问题。
我面向百度编程,一搜索果真发现各种结果,看来确实是个经典的问题了。
以前也了解过 它符合的是银行家的算法(四舍六入五成双),啥意思呢就是: 四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一 (当舍去位的数值 ≤4 时舍去,当它 ≥6 时加上,可当它 =5 时,则根据 5 后面的数字来定;当 5 后有非零数字时,舍 5 入 1;当 5 后无有效数字时,需要再分两种情况:5 前为偶数,舍 5 不进;5 前为奇数,舍 5 入 1)

更具上面的规则,我进行了一番测试,结果又发现了问题:

const a = 3.15 
const b = 3.25
console.log(a.toFixed(1));  // 3.1
console.log(b.toFixed(1));  // 3.3 

怎么回事?变量a  5前为奇数 没有进1;变量b 前为偶数 也没有舍弃反而进了 1 ; 

猜测 1: js 精度问题引起的
我们都知道
JavaScript 中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。为什么呢,因为这样节省存储空间。

也就是说 17652.19 + 7673.78 = 25325.969999999998。其实最简单的例子是 0.1+0.2 = 0.30000000000000004

0.1的二进制表示的是一个无限循环小数,该版本的 JS 采用的是浮点数标准需要对这种无限循环的二进制进行截取,从而导致了精度丢失,造成了0.1不再是0.1,截取之后0.1变成了 0.100…001,0.2变成了0.200…002。所以两者相加的数大于0.3。

将0.1转换成为二进制加上0.2的二进制会是53位,但是二进制的最大位数是52位取近似值。

上述的 3.25 其实类似于3.2500000002 存在尾数 所以按照tofixed 的算法 就应该是 3.3,但是3.15呢 难道是类似于 3.14999999999998 ? 这里我又引入一个 decimal.JS 来解决js 精度的问题。

想必大家在用js 处理 数字的 加减乘除的时候,或许都有遇到过 精度不够的问题

还有那些经典的面试题 02+0.1 == 0.3

至于原因,那就是 js 计算底层用的 是 ,精度上有限制

那么,Decimal.js 就是帮助我们解决 js中的精度失准的问题


Decimal 的引入 与 加减乘除
引入
npm install --save decimal.js  // 安装
import Decimal from "decimal.js"  // 具体文件中引入


let a = 1
let b = 6 
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).add(new Decimal(b)) 
let res = Decimal(a).add(Decimal(b)) 

let a = "4"
let b = "8"
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).sub(new Decimal(b)) 
let res = Decimal(a).sub(Decimal(b)) 

let a = 1
let b = 6 
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).mul(new Decimal(b)) 
let res = Decimal(a).mul(Decimal(b)) 

let a = 1
let b = 6 
// a 与 b 可以是 任何类型,Decimal 内部会自己处理兼容
// 下面两种都可以 可以带 new 也不可以不带 new
let res = new Decimal(a).div(new Decimal(b)) 
let res = Decimal(a).div(Decimal(b)) 

注意:
上面的结果是 一个 Decimal 对象,你可以转换成 Number 或则 String

let res = Decimal(a).div(Decimal(b)).toNumber()  // 结果转换成 Number
let res = Decimal(a).div(Decimal(b)).toString()  // 结果转换成 String
1
2
关于保存几位小数相关

//查看有几位小数 (注意不计算 小数点 最后 末尾 的 0)
y = new Decimal(987000.000)
y.sd()                                   // '3' 有效位数
y.sd(true)                               // '6' 总共位数

// 保留 多少个位数 (小数位 会补0)
x = 45.6
x.toPrecision(5)                         // '45.600'

// 保留 多少位有效位数(小数位 不会补0,是计算的有效位数)
x = new Decimal(9876.5)
x.toSignificantDigits(6)                 // '9876.5' 不会补0 只是针对有效位数

// 保留几位小数 , 跟 js 中的 number 一样
toFixed
x = 3.456
// 向下取整
x.toFixed(2, Decimal.ROUND_DOWN)  // '3.45' (舍入模式 向上0 向下1 四舍五入 4,7)
// 向上取整
Decimal.ROUND_UP 

//四舍五入
ROUND_HALF_UP



计算

let a = 1
let b = 6 
//加
let res = new Decimal(a).add(new Decimal(b))  //得到的值是一个Decimal对象 需要转换
let res1 = new Decimal(a).add(new Decimal(b)).toNumber() //结果转换成number
let res2 = new Decimal(a).add(new Decimal(b)).toString() //结果转换成string
//下面同上操作
//减
let res = new Decimal(a).sub(new Decimal(b))
//乘
let res = new Decimal(a).mul(new Decimal(b))
//除
let res = new Decimal(a).div(new Decimal(b))

固定取两位小数,其他抹去

/**
 * 取2位小数(可自定义)
 * 
 * @param num1 参数1
 * @param num2 参数2
 * @param status 1(+) 2(-) 3(*)
 * @param num 小数后 num-1位
 * @returns 
 */
export const multiply = (num1, num2, status,num=3) => {
  let sum = ''
  if (status === 1) {
    sum = new Decimal(parseFloat(num1)).add(new Decimal(parseFloat(num2))).toFixed(3).toString()
    return +sum.substring(0, sum.indexOf(".") + 3)
  } else if (status === 3) {
    sum = new Decimal(parseFloat(num1)).mul(new Decimal(parseFloat(num2))).toFixed(3).toString()

    return +sum.substring(0, sum.indexOf(".") + 3)
  }
}



但是

created() {
    // const a = 2.998;
    // const b = 8.037;
    // var g = parseFloat(a + b);
    // var h = g.toFixed(2);
    // // 加法
    // let c = new Decimal(a).add(new Decimal(b)).toNumber();
    // let i = c.toFixed(2);
    // // 减法
    // let d = new Decimal(a).sub(new Decimal(b));
    // // 乘法
    // let e = new Decimal(a).mul(new Decimal(b));
    // // 除法
    // let f = new Decimal(a).div(new Decimal(b));
    // console.log("---->>>", c, d, e, f, g, h, i);
    // console.log(
    //   "---->>>",
    //   new Decimal(c),
    //   new Decimal(d),
    //   new Decimal(e),
    //   new Decimal(f)
    // );
    var z = 3.25;
    var x = z.toFixed(3);
    var xx = parseFloat(x).toFixed(1);
    var o = new Decimal(z);
    let y = new Decimal(z).toNumber();
    var q = y.toFixed(1);
    console.log("---->>>>sss", o, y, q, x, xx);
  }

 // ---->>>>sss Decimal {s: 1, e: 0, d: Array(2), constructor: ƒ} 3.25 3.3 3.250 3.3

还是不对, 这就可能不单单是精度的问题了  又要抓头发了 。。。 




再经过一番探索,总算是有点收获了,下面就得来看看 ECMAScript 规范对该方法的定义了,有时候回归规范才是最靠谱的方式。

上图是关于整个 toFixed 方法的定义,不过是翻译后的版本,会有出入但差别不大,也可以点击上面的链接查看原文,我们主要关注图中红框部分,通过公式来计算舍去位数值。

下面我们举个例子。测试一下:

eg:
console.log(1.0000005.toFixed(6)); // 1.000001 正确
console.log(1.00000005.toFixed(7)); // 1.0000000 错误 


首先,根据红框的条件,x<10^21,1.0000005 与 1.00000005 都是小于 10^21 的,所以我们直接可以使用公式 n / 10^ - x 来玩耍。

我们先用 x=1.0000005 代入公式来看看情况:

// 假设n1
var n1 = 1000000;
var x = 1.0000005;
var f = 6;
console.log((n1 / Math.pow(10, f) - x)); // -5.00000000069889e-7
 
// 假设n2
var n2 = 1000001;
var x = 1.0000005;
var f = 6;
console.log((n2 / Math.pow(10, f) - x)); // 4.999999998478444e-7

由结果可知,当 n1=1000001 时,得到的结果取最接近 0 的值,故:

console.log(1.0000005.toFixed(6)); // 1.000001 正确
1
再来试试当 x=1.00000005 代入公式:

// 假设n1
var n1 = 10000000;
var x = 1.00000005;
var f = 7;
console.log((n1 / Math.pow(10,f) - x)); // -4.9999999918171056e-8
 
// 假设n2
var n2 = 10000001;
var x = 1.00000005;
var f = 7;
console.log((n2 / Math.pow(10,f) - x)); // 5.000000014021566e-8

由结果可知,当 n2=10000001 时,得到的结果取最接近 0 的值,故:

    
console.log(1.00000005.toFixed(7)); // 1.0000000 错误
1
2
总的来说,上面例子就是教你如何通过规范定义的公式计算出结果而已,如果你看得懂规范,那么直接去代入也是没有问题的。
————————————————
版权声明:本文为CSDN博主「斯昂」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Leon_940002463/article/details/124094588

猜你喜欢

转载自blog.csdn.net/wwf1225/article/details/131822301