我想最多四舍五入两位小数,但只有在必要时。
输入:
10
1.7777777
9.1
输出:
10
1.78
9.1
如何在JavaScript中执行此操作?
我想最多四舍五入两位小数,但只有在必要时。
输入:
10
1.7777777
9.1
输出:
10
1.78
9.1
如何在JavaScript中执行此操作?
当前回答
这个问题很复杂。
假设我们有一个函数roundTo2DP(num),它将浮点作为参数,并返回一个舍入到小数点后2位的值。这些表达式的求值结果应该是什么?
舍入到2DP(0.014999999999999999)舍入到2DP(0.015000000000000001)舍入到2DP(0.015)
“显而易见”的答案是,第一个例子应该四舍五入到0.01(因为它比0.02更接近0.01),而其他两个应该四舍二入到0.02(因为0.015000000000000001比0.01更接近0.02,因为0.015正好在两者之间的中间位置,并且有一个数学惯例,这样的数字会四舍五舍五入)。
你可能已经猜到了,问题是roundTo2DP不可能实现为给出这些显而易见的答案,因为传递给它的所有三个数字都是相同的数字。IEEE 754二进制浮点数(JavaScript使用的类型)不能准确表示大多数非整数,因此上面的三个数字文本都四舍五入到附近的有效浮点数。这个数字恰好是
0.01499999999999999944488848768742172978818416595458984375
其比0.02更接近0.01。
您可以在浏览器控制台、Nodeshell或其他JavaScript解释器中看到这三个数字都是相同的。只需比较它们:
> 0.014999999999999999 === 0.0150000000000000001
true
所以当我写m=0.01500000000000000001时,我最终得到的m的精确值更接近0.01,而不是0.02。然而,如果我把m转换成字符串。。。
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
……我得到了0.015,应该四舍五入到0.02,这显然不是我之前说过的所有这些数字都完全相等的56位小数。那么这是什么神奇的东西呢?
答案可以在ECMAScript规范的7.1.12.1节:适用于Number类型的ToString中找到。这里列出了将数字m转换为字符串的规则。关键部分是第5点,其中生成一个整数s,其数字将用于m的字符串表示:
设n、k和s为整数,使得k≥1,10k-1≤s<10k,s×10n-k的数值为m,k尽可能小。注意,k是s的十进制表示中的位数,s不能被10整除,并且s的最低有效位数不一定由这些标准唯一确定。
这里的关键部分是“k尽可能小”的要求。该要求相当于一个要求,即给定一个数字m,字符串(m)的值必须具有尽可能少的位数,同时仍然满足数字(String(m))==m的要求。由于我们已经知道0.015==0.015000000000000001,现在很清楚为什么字符串(0.01500000000000001)==“0.015”必须为真。
当然,这些讨论都没有直接回答roundTo2DP(m)应该返回什么。如果m的精确值为0.014999999999994448848768742172978818416595458984375,但其字符串表示为“0.015”,那么当我们将其舍入到两位小数时,正确答案是什么?
对此没有单一的正确答案。这取决于您的用例。在以下情况下,您可能希望遵守字符串表示法并向上舍入:
所表示的值本质上是离散的,例如以3位小数货币(如第纳尔)表示的货币量。在这种情况下,像0.015这样的数字的真值是0.015,它在二进制浮点中得到的0.0149999999…表示是舍入误差。(当然,许多人会合理地争辩说,应该使用十进制库来处理这些值,而不要首先将它们表示为二进制浮点数。)该值由用户键入。在这种情况下,输入的精确十进制数比最近的二进制浮点表示更为“真”。
另一方面,当你的值来自一个固有的连续刻度时,你可能希望尊重二进制浮点值并向下舍入-例如,如果它是传感器的读数。
这两种方法需要不同的代码。为了尊重数字的字符串表示法,我们可以(用相当精细的代码)实现我们自己的舍入,该舍入直接作用于字符串表示法(一个数字一个数字),使用的算法与学校教你如何舍入数字时使用的算法相同。下面是一个例子,它尊重OP的要求,即“仅在必要时”通过在小数点后去掉尾随零来将数字表示为2位小数;当然,你可能需要根据你的具体需要调整它。
/**
* Converts num to a decimal string (if it isn't one already) and then rounds it
* to at most dp decimal places.
*
* For explanation of why you'd want to perform rounding operations on a String
* rather than a Number, see http://stackoverflow.com/a/38676273/1709587
*
* @param {(number|string)} num
* @param {number} dp
* @return {string}
*/
function roundStringNumberWithoutTrailingZeroes (num, dp) {
if (arguments.length != 2) throw new Error("2 arguments required");
num = String(num);
if (num.indexOf('e+') != -1) {
// Can't round numbers this large because their string representation
// contains an exponent, like 9.99e+37
throw new Error("num too large");
}
if (num.indexOf('.') == -1) {
// Nothing to do
return num;
}
if (num[0] == '-') {
return "-" + roundStringNumberWithoutTrailingZeroes(num.slice(1), dp)
}
var parts = num.split('.'),
beforePoint = parts[0],
afterPoint = parts[1],
shouldRoundUp = afterPoint[dp] >= 5,
finalNumber;
afterPoint = afterPoint.slice(0, dp);
if (!shouldRoundUp) {
finalNumber = beforePoint + '.' + afterPoint;
} else if (/^9+$/.test(afterPoint)) {
// If we need to round up a number like 1.9999, increment the integer
// before the decimal point and discard the fractional part.
// We want to do this while still avoiding converting the whole
// beforePart to a Number (since that could cause loss of precision if
// beforePart is bigger than Number.MAX_SAFE_INTEGER), so the logic for
// this is once again kinda complicated.
// Note we can (and want to) use early returns here because the
// zero-stripping logic at the end of
// roundStringNumberWithoutTrailingZeroes does NOT apply here, since
// the result is a whole number.
if (/^9+$/.test(beforePoint)) {
return "1" + beforePoint.replaceAll("9", "0")
}
// Starting from the last digit, increment digits until we find one
// that is not 9, then stop
var i = beforePoint.length - 1;
while (true) {
if (beforePoint[i] == '9') {
beforePoint = beforePoint.substr(0, i) +
'0' +
beforePoint.substr(i+1);
i--;
} else {
beforePoint = beforePoint.substr(0, i) +
(Number(beforePoint[i]) + 1) +
beforePoint.substr(i+1);
break;
}
}
return beforePoint
} else {
// Starting from the last digit, increment digits until we find one
// that is not 9, then stop
var i = dp-1;
while (true) {
if (afterPoint[i] == '9') {
afterPoint = afterPoint.substr(0, i) +
'0' +
afterPoint.substr(i+1);
i--;
} else {
afterPoint = afterPoint.substr(0, i) +
(Number(afterPoint[i]) + 1) +
afterPoint.substr(i+1);
break;
}
}
finalNumber = beforePoint + '.' + afterPoint;
}
// Remove trailing zeroes from fractional part before returning
return finalNumber.replace(/0+$/, '')
}
示例用法:
> roundStringNumberWithoutTrailingZeroes(1.6, 2)
'1.6'
> roundStringNumberWithoutTrailingZeroes(10000, 2)
'10000'
> roundStringNumberWithoutTrailingZeroes(0.015, 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes('0.015000', 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes(1, 1)
'1'
> roundStringNumberWithoutTrailingZeroes('0.015', 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes(0.01499999999999999944488848768742172978818416595458984375, 2)
'0.02'
> roundStringNumberWithoutTrailingZeroes('0.01499999999999999944488848768742172978818416595458984375', 2)
'0.01'
> roundStringNumberWithoutTrailingZeroes('16.996', 2)
'17'
上面的函数可能是您想要使用的,以避免用户看到他们输入的数字被错误舍入。
(作为替代方案,您也可以尝试round10库,该库提供了一个行为类似的函数,但实现却大相径庭。)
但是如果你有第二种数字-一个取自连续刻度的值,没有理由认为小数位数少的近似小数表示比小数位数多的更准确,那会怎样呢?在这种情况下,我们不想尊重String表示,因为该表示(如规范中所解释的)已经是舍入的;我们不想犯“0.014999999…375舍入到0.015,等于0.02,所以0.014999999。375舍入到0.02”的错误。
这里我们可以简单地使用内置的toFixed方法。注意,通过对toFixed返回的字符串调用Number(),我们得到了一个字符串表示没有尾随零的Number(这要归功于JavaScript计算Number的字符串表示的方式,前面在这个答案中讨论过)。
/**
* Takes a float and rounds it to at most dp decimal places. For example
*
* roundFloatNumberWithoutTrailingZeroes(1.2345, 3)
*
* returns 1.234
*
* Note that since this treats the value passed to it as a floating point
* number, it will have counterintuitive results in some cases. For instance,
*
* roundFloatNumberWithoutTrailingZeroes(0.015, 2)
*
* gives 0.01 where 0.02 might be expected. For an explanation of why, see
* http://stackoverflow.com/a/38676273/1709587. You may want to consider using the
* roundStringNumberWithoutTrailingZeroes function there instead.
*
* @param {number} num
* @param {number} dp
* @return {number}
*/
function roundFloatNumberWithoutTrailingZeroes (num, dp) {
var numToFixedDp = Number(num).toFixed(dp);
return Number(numToFixedDp);
}
其他回答
我只是想根据前面提到的答案分享我的方法:
让我们创建一个函数,将任何给定的数值舍入到给定的小数位数:
function roundWDecimals(n, decimals) {
if (!isNaN(parseFloat(n)) && isFinite(n)) {
if (typeof(decimals) == typeof(undefined)) {
decimals = 0;
}
var decimalPower = Math.pow(10, decimals);
return Math.round(parseFloat(n) * decimalPower) / decimalPower;
}
return NaN;
}
并为数字原型引入一种新的“舍入”方法:
Object.defineProperty(Number.prototype, 'round', {
enumerable: false,
value: function(decimals) {
return roundWDecimals(this, decimals);
}
});
您可以测试它:
函数舍入WDecimals(n,小数){if(!isNaN(parseFloat(n))&&isFinite(n){if(typeof(小数)==typeof(未定义)){小数=0;}var decimalPower=数学.pow(10,小数);return Math.round(parseFloat(n)*decimalPower)/decimalPower;}返回NaN;}Object.defineProperty(Number.prototype,'round'{可枚举:false,值:函数(小数){返回舍入WDecimals(this,小数);}});var舍入=[{num:10,小数:2},{num:1.7777777,小数:2},{num:9.1,小数:2},{num:55.55,小数:1},{num:55.549,小数:1},{num:55,小数:0},{num:54.9,小数:0},{num:-55.55,小数:1},{num:-55.551,小数:1},{num:-55,小数:0},{num:1.005,小数:2},{num:1.005,小数:2},{num:198000000007,小数:2},],table='<table border=“1”><tr><th>Num</th><th>小数</th><th>结果</th></tr>';$.each(roundables,function(){表+='<tr>'+“<td>”+此.num+“</td>”+“<td>”+此小数+“</td>”+'<td>'+this.num.round(this.decimals)+'</td>'+'</tr>';});table+=“</table>”;$('.results').append(表);<script src=“https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js“></script><div class=“results”></div>
这项看似简单的任务面临的最大挑战是,我们希望它能够产生心理预期的结果,即使输入包含最小的舍入误差(更不用说计算中会出现的误差)。如果我们知道实际结果正好是1.005,那么我们预计舍入到两位数会得到1.01,即使1.005是一个带有大量舍入误差的大型计算的结果。
当处理floor()而不是round()时,问题变得更加明显。例如,当删除33.3点后面的最后两位数字后的所有内容时,我们肯定不会期望得到33.29,但这就是结果:
console.log(数学楼层(33.3*100)/100)
在简单的情况下,解决方案是对字符串而不是浮点数执行计算,从而完全避免舍入错误。然而,这个选项在第一次非平凡的数学运算(包括大多数除法运算)时失败,而且速度很慢。
当对浮点数进行操作时,解决方案是引入一个参数,该参数指定我们愿意偏离实际计算结果的量,以便输出心理预期的结果。
var round=函数(num,数字=2,compensateErrors=2){如果(num<0){return this.round(-num,数字,compensateErrors);}const pow=数学.pow(10,数字);return(数学舍入(num*pow*(1+compensateErrors*Number.EPSILON))/pow);}/*---测试---*/console.log(“本线程中提到的边缘案例:”)var值=[0.015,1.005,5.555,156893.145,362.42499999999995,1.275,1.277499,1.2345678e+2,2.175,5.015,58.9*0.15];值。对于每个((n)=>{console.log(n+“->”+圆(n));console.log(-n+“->”+圆形(-n));});console.log(“\n对于太大以至于无法在计算精度范围内执行舍入的数字,只有基于字符串的计算才有帮助。”)console.log(“标准:”+圆形(1e+19));console.log(“补偿=1:”+圆(1e+19,2,1));console.log(“有效无补偿:”+round(1e+19,2,0.4));
注意:Internet Explorer不知道Number.EPSILON。如果您仍然需要支持它,那么您可以使用垫片,或者自己定义特定浏览器系列的常量。
您应该使用:
Math.round( num * 100 + Number.EPSILON ) / 100
似乎没有人知道数字EPSILON。
此外,值得注意的是,这并不像某些人所说的那样是JavaScript的怪异之处。
这就是浮点数在计算机中的工作方式。与99%的编程语言一样,JavaScript没有自制的浮点数;它依赖于CPU/FPU。计算机使用二进制,在二进制中,没有像0.1这样的数字,而只是二进制的近似值。为什么?出于同样的原因,1/3不能用十进制写:它的值是0.33333333……无穷大为三。
这里是Number.EPSILON。这个数字是1和双精度浮点数字中存在的下一个数字之间的差值。就是这样:在1和1+number.EPSILON之间没有数字。
编辑:
正如评论中所问的,让我们澄清一件事:添加Number.EPSILON仅当要舍入的值是算术运算的结果时才相关,因为它可以吞下一些浮点误差增量。
当值来自直接来源(例如:文字、用户输入或传感器)时,它不起作用。
编辑(2019):
像@maganap和一些人指出的那样,最好在相乘之前加上Number.EPSILON:
Math.round( ( num + Number.EPSILON ) * 100 ) / 100
编辑(2019年12月):
最近,我使用了一个类似于此的函数来比较epsilon感知的数字:
const ESPILON_RATE = 1 + Number.EPSILON ;
const ESPILON_ZERO = Number.MIN_VALUE ;
function epsilonEquals( a , b ) {
if ( Number.isNaN( a ) || Number.isNaN( b ) ) {
return false ;
}
if ( a === 0 || b === 0 ) {
return a <= b + EPSILON_ZERO && b <= a + EPSILON_ZERO ;
}
return a <= b * EPSILON_RATE && b <= a * EPSILON_RATE ;
}
我的用例是我多年来开发的断言+数据验证库。
事实上,在代码中,我使用的是ESPILON_RATE=1+4*数字.EPSILON和EPSILON_ZERO=4*数字.MIN_VALUE(四倍于EPSILON),因为我想要一个足够宽松的等式检查器来累积浮点错误。
到目前为止,它看起来很适合我。我希望这会有所帮助。
有两种方法可以做到这一点。对于像我这样的人,Lodash的变体
function round(number, precision) {
var pair = (number + 'e').split('e')
var value = Math.round(pair[0] + 'e' + (+pair[1] + precision))
pair = (value + 'e').split('e')
return +(pair[0] + 'e' + (+pair[1] - precision))
}
用法:
round(0.015, 2) // 0.02
round(1.005, 2) // 1.01
如果您的项目使用jQuery或Lodash,您也可以在库中找到适当的舍入方法。
精确的舍入方法。来源:Mozilla
(function(){
/**
* Decimal adjustment of a number.
*
* @param {String} type The type of adjustment.
* @param {Number} value The number.
* @param {Integer} exp The exponent (the 10 logarithm of the adjustment base).
* @returns {Number} The adjusted value.
*/
function decimalAdjust(type, value, exp) {
// If the exp is undefined or zero...
if (typeof exp === 'undefined' || +exp === 0) {
return Math[type](value);
}
value = +value;
exp = +exp;
// If the value is not a number or the exp is not an integer...
if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) {
return NaN;
}
// Shift
value = value.toString().split('e');
value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
// Shift back
value = value.toString().split('e');
return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
}
// Decimal round
if (!Math.round10) {
Math.round10 = function(value, exp) {
return decimalAdjust('round', value, exp);
};
}
// Decimal floor
if (!Math.floor10) {
Math.floor10 = function(value, exp) {
return decimalAdjust('floor', value, exp);
};
}
// Decimal ceil
if (!Math.ceil10) {
Math.ceil10 = function(value, exp) {
return decimalAdjust('ceil', value, exp);
};
}
})();
示例:
// Round
Math.round10(55.55, -1); // 55.6
Math.round10(55.549, -1); // 55.5
Math.round10(55, 1); // 60
Math.round10(54.9, 1); // 50
Math.round10(-55.55, -1); // -55.5
Math.round10(-55.551, -1); // -55.6
Math.round10(-55, 1); // -50
Math.round10(-55.1, 1); // -60
Math.round10(1.005, -2); // 1.01 -- compare this with Math.round(1.005*100)/100 above
// Floor
Math.floor10(55.59, -1); // 55.5
Math.floor10(59, 1); // 50
Math.floor10(-55.51, -1); // -55.6
Math.floor10(-51, 1); // -60
// Ceil
Math.ceil10(55.51, -1); // 55.6
Math.ceil10(51, 1); // 60
Math.ceil10(-55.59, -1); // -55.5
Math.ceil10(-59, 1); // -50