四舍五入保留小数

1,207 阅读2分钟

如果叫你四舍五入保留n位小数,你会用哪种方法呢,下面笔者会介绍几种方法供你参考。

原生 toFixed

嗯,如果用原生方法就能解决问题,那么写这篇文章有啥用 -。-。 toFixed 的缺点很明显,就是大多数浏览器无法准确计算以5为尾部的小数

let number = 123.345;
number.toFixed(2); // 123.34 chrome, firefox, safari 都是显示这个

Math.round

Math.round 可以对数字进行四舍五入成整数,可以利用这点进行一些简单的乘除运算达到保留n位小数点的效果。

function toFixed(num,decimal){
    if(isNaN(num)){
        return 0;
    }
    num = num-0;
    var p1 = Math.pow(10, decimal + 1);
    var p2 = Math.pow(10, decimal);
    console.log(num * p1 / 10);
    console.log(Math.round(num * p1 / 10));
    return (Math.round(num * p1 / 10) / p2).toFixed(decimal); 
}

上面代码的逻辑很简单,就是把一个数字num乘于10的(n+1)倍数再除以10转成只有一位小数的值,再用math.round进行四舍五入,最后除以10的(n)倍数得到对应的n位小数点的值。

上面的方法简单也实用,可惜有bug...看看下面的例子:

toFixed(4100.065,2); // 4100.06
// 4100065/10 = 410006.49999999994

原因是就是js编程语言的小数点精度问题,在上面除以10的过程中,某些数值产生了精度问题。

字符串处理

转成字符串处理可以避开精度问题,但是要自己模拟四舍五入。下面提供一个思路:

  1. 把数字转成字符,并记录.小数点的位置;如果没有小数点,则直接在后面补0
  2. 通过小数点的位置,计算出原数字有多少位小数(oldPointNum);把小数点去掉并且把字符串转成数组
  3. 通过比较oldPointNum和n(要保留的位数);如果oldPointNum<n,直接补0;否则下一步
  4. 比较数组倒数(i =oldPointNum-n)的数字是否>=5;是则进一位;i++;循环该步骤(核心),用这一步来模拟四舍五入
const toFixed = (number, n) => {
  let numberStr = number + "";
  let reg = /^(-|\+)?(\d+(\.\d*)?|\.\d+)$/i;
  if(!reg.test(numberStr)) {
      console.error('输入的数字格式不对');
      return;
  }
  let sign = numberStr.charAt(0) === '-' ? (numberStr=numberStr.slice(1),-1):1; // 判断是否是负数
  let pointIndex = numberStr.indexOf("."); // 记录小数点的位置
  if (pointIndex > -1) {
    numberStr = numberStr.replace(".", "");
  } else { // 没有小数点直接添加补0;
    numberStr += ".";
    numberStr+=new Array(n).join('0');
    return numberStr;
  }
  let numberArray = numberStr.split(""); //转成数组
  let len = numberArray.length;
  let oldPointNum = len - pointIndex; // 获取原数据有多少位小数
  if (oldPointNum < n) { // 要保留的小数点比原来的要大,直接补0
    while (n - oldPointNum > 0) {
      numberArray.push(0);
      n--;
    }
  } else if (oldPointNum > n) { // 模拟四舍五入
    
    let i = oldPointNum - n; // 从倒数第i个数字开始比较
    let more = numberArray[len - i] >= 5 ? true : false;
    while (more) {
      i++;
      more = +numberArray[len - i] + 1 === 10 ? true : false; // 进位后判断是否等于10,是则继续进位
      numberArray[len - i] = more&&i!==(len+1) ? 0 : +numberArray[len - i] + 1; // 其他位置的数字进位直接变成0,第一位的例外
      console.log(i, len);
    }
    numberArray.length = len- (oldPointNum-n); // 截取多余的小数
  }
  numberArray.splice(pointIndex, 0, ".");
  return sign===-1?'-'+numberArray.join(""):numberArray.join("");
};

另外提供另一个版本,思路差不多,但是更多使用了正则:

const toFixed = (number,n)=>{
  let numberStr = number + "";
  if(!n) n=0;
  if(numberStr.indexOf('.') === -1) numberStr+='.';
  numberStr += new Array(n + 1).join("0");
  let reg = new RegExp("^(-|\\+)?(\\d+(\\.\\d{0," + (n + 1) + "})?)\\d*$"); 
  const matches = numberStr.match(reg);
  if(!matches){
      console.error('输入的数字格式不对');
      return;
  }
  console.log(matches);
  let sign = matches[1] || '';
  let pointNum = matches[3];
  numberStr = '0'+matches[2];
  let isTop = true;
  let len = pointNum.length;
  if(len === n+2){
    pointNum = numberStr.match(/\d/g);
    len = pointNum.length;
    if((+pointNum[len-1]) >=5){
      for(let i=len-2;i>=0;i--){
        pointNum[i] = (+pointNum[i]) +1;
        if(pointNum[i] === 10){
          pointNum[i] = 0;
          isTop = i!==1;
        }else break;
      }
    }

    numberStr = pointNum.join('').replace(new RegExp("(\\d+)(\\d{" + n + "})\\d$"),'$1.$2');
  }
  if(isTop) numberStr = numberStr.slice(1);
  return (sign+numberStr)
  
}

总结

总的来说,原生toFixed不靠谱;Math.round模拟的代码量少且简单,但是会有精度问题;用字符串模拟的代码量大(代码可能存在bug...),但是不会有精度问题。就看你想用哪种喽