logo
Published on

如何正确地将数字四舍五入到最多两位小数

Authors
  • Name
    Twitter

在编程中,数字的四舍五入操作是一个常见但容易出错的问题。因为浮点数的精度问题,简单的数学运算往往无法满足精度要求,这可能导致无法预料的错误。本文将讨论几种在JavaScript中有效的四舍五入方法。

简单实现

通常我们使用Math.round函数进行简单的四舍五入:

function naiveRound(num, decimalPlaces = 0) {
  var p = Math.pow(10, decimalPlaces)
  return Math.round(num * p) / p
}

console.log(naiveRound(1.245, 2)) // 1.25 (正确)
console.log(naiveRound(1.255, 2)) // 1.25 (错误,应该是 1.26)

在这个简单的实现中,由于浮点数精度问题,当数字等于0.5时,有时可能无法得到预期的结果。比如,上例中的1.255被错误地四舍五入为1.25。

更佳实现

指数符号法

通过将数字转换为字符串形式的指数表示法来进行四舍五入,可以解决一些浮点数精度问题。

function exponentialRound(num, decimalPlaces = 0) {
  num = Math.round(num + 'e' + decimalPlaces)
  return Number(num + 'e' + -decimalPlaces)
}

console.log(exponentialRound(1.005, 2)) // 1.01
console.log(exponentialRound(2.175, 2)) // 2.18

近似四舍五入法

通过定义一个自定义的四舍五入函数进行“近似相等”测试,可以决定是否是到中间值的四舍五入。

function nearlyEqualRound(num, decimalPlaces = 0) {
  if (num < 0) return -nearlyEqualRound(-num, decimalPlaces)
  var p = Math.pow(10, decimalPlaces)
  var n = num * p
  var f = n - Math.floor(n)
  var e = Number.EPSILON * n

  return f >= 0.5 - e ? Math.ceil(n) / p : Math.floor(n) / p
}

console.log(nearlyEqualRound(1.005, 2)) // 1.01
console.log(nearlyEqualRound(2.175, 2)) // 2.18

Number.EPSILON

通过应用最小的浮点值进行四舍五入前的修正,可以解决浮点数乘以10的幂时误差问题。

function epsilonRound(num, decimalPlaces = 0) {
  var p = Math.pow(10, decimalPlaces)
  var n = num * p * (1 + Number.EPSILON)
  return Math.round(n) / p
}

console.log(epsilonRound(1.005, 2)) // 1.01
console.log(epsilonRound(2.175, 2)) // 2.18

双重四舍五入法

通过两次四舍五入消除中间计算中的浮点数舍入误差。

function doubleRound(num, decimalPlaces = 0) {
  var p = Math.pow(10, decimalPlaces)
  var n = (num * p).toPrecision(15)
  return Math.round(n) / p
}

console.log(doubleRound(1.005, 2)) // 1.01
console.log(doubleRound(2.175, 2)) // 2.18

使用任意精度的JavaScript库

例如,使用decimal.js库,可以更精确地进行浮点数运算。

// 需要引入decimal.js库
function decimalJsRound(num, decimalPlaces = 0) {
  return new Decimal(num).toDecimalPlaces(decimalPlaces).toNumber()
}

console.log(decimalJsRound(1.005, 2)) // 1.01
console.log(decimalJsRound(2.175, 2)) // 2.18

其他解决方案

可以采用字符串转换或完全数学方法来避免任何字符串转换操作。本文在这里列出了一些不同的方法和测试用例。

字符串转换方法(解决方案1)

var DecimalPrecision = (function () {
  var decimalAdjust = function myself(type, num, decimalPlaces) {
    if (type === 'round' && num < 0) return -myself(type, -num, decimalPlaces)
    var shift = function (value, exponent) {
      value = (value + 'e').split('e')
      return +(value[0] + 'e' + (+value[1] + (exponent || 0)))
    }
    var n = shift(num, +decimalPlaces)
    return shift(Math[type](n), -decimalPlaces)
  }
  return {
    round: function (num, decimalPlaces) {
      return decimalAdjust('round', num, decimalPlaces)
    },
    ceil: function (num, decimalPlaces) {
      return decimalAdjust('ceil', num, decimalPlaces)
    },
    floor: function (num, decimalPlaces) {
      return decimalAdjust('floor', num, decimalPlaces)
    },
    trunc: function (num, decimalPlaces) {
      return decimalAdjust('trunc', num, decimalPlaces)
    },
    toFixed: function (num, decimalPlaces) {
      return decimalAdjust('round', num, decimalPlaces).toFixed(decimalPlaces)
    },
  }
})()

console.log(DecimalPrecision.round(1.005, 2)) // 1.01
console.log(DecimalPrecision.round(39.425, 2)) // 39.43

纯数学(Number.EPSILON)方法(解决方案2)

var DecimalPrecision2 = (function () {
  return {
    round: function (num, decimalPlaces) {
      var p = Math.pow(10, decimalPlaces || 0)
      var n = num * p * (1 + Number.EPSILON)
      return Math.round(n) / p
    },
    ceil: function (num, decimalPlaces) {
      var p = Math.pow(10, decimalPlaces || 0)
      var n = num * p * (1 - Math.sign(num) * Number.EPSILON)
      return Math.ceil(n) / p
    },
    floor: function (num, decimalPlaces) {
      var p = Math.pow(10, decimalPlaces || 0)
      var n = num * p * (1 + Math.sign(num) * Number.EPSILON)
      return Math.floor(n) / p
    },
    trunc: function (num, decimalPlaces) {
      return (num < 0 ? this.ceil : this.floor)(num, decimalPlaces)
    },
    toFixed: function (num, decimalPlaces) {
      return this.round(num, decimalPlaces).toFixed(decimalPlaces)
    },
  }
})()

console.log(DecimalPrecision2.round(1.005, 2)) // 1.01
console.log(DecimalPrecision2.ceil(1e-8, 2) === 0.01)

双重四舍五入法(解决方案3)

var DecimalPrecision3 = (function () {
  var decimalAdjust = function myself(type, num, decimalPlaces) {
    if (type === 'round' && num < 0) return -myself(type, -num, decimalPlaces)
    var p = Math.pow(10, decimalPlaces || 0)
    var n = parseFloat((num * p).toPrecision(15))
    return Math[type](n) / p
  }
  return {
    round: function (num, decimalPlaces) {
      return decimalAdjust('round', num, decimalPlaces)
    },
    ceil: function (num, decimalPlaces) {
      return decimalAdjust('ceil', num, decimalPlaces)
    },
    floor: function (num, decimalPlaces) {
      return decimalAdjust('floor', num, decimalPlaces)
    },
    trunc: function (num, decimalPlaces) {
      return decimalAdjust('trunc', num, decimalPlaces)
    },
    toFixed: function (num, decimalPlaces) {
      return decimalAdjust('round', num, decimalPlaces).toFixed(decimalPlaces)
    },
  }
})()

console.log(DecimalPrecision3.round(1.005, 2)) // 1.01
console.log(DecimalPrecision3.round(39.425, 2)) // 39.43

本文介绍了多种方法来解决JavaScript中将数字四舍五入到两位小数的常见问题。每种方法都有其优缺点,选择哪种方法可以根据具体需求和性能考虑。