- Published on
如何正确地将数字四舍五入到最多两位小数
- Authors
- Name
在编程中,数字的四舍五入操作是一个常见但容易出错的问题。因为浮点数的精度问题,简单的数学运算往往无法满足精度要求,这可能导致无法预料的错误。本文将讨论几种在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中将数字四舍五入到两位小数的常见问题。每种方法都有其优缺点,选择哪种方法可以根据具体需求和性能考虑。