我想最多四舍五入两位小数,但只有在必要时。
输入:
10
1.7777777
9.1
输出:
10
1.78
9.1
如何在JavaScript中执行此操作?
我想最多四舍五入两位小数,但只有在必要时。
输入:
10
1.7777777
9.1
输出:
10
1.78
9.1
如何在JavaScript中执行此操作?
当前回答
通常,小数舍入是通过缩放来完成的:round(num*p)/p
天真的实施
将以下函数与中间数一起使用,您将获得预期的上舍入值,或有时根据输入获得下舍入值。
舍入中的这种不一致可能会在客户端代码中引入难以检测的错误。
函数naiveRound(num,decimalPlaces=0){var p=数学.pow(10,小数位数);return数学舍入(num*p)/p;}console.log(naiveRound(1.245,2));//1.25正确(按预期四舍五入)console.log(naiveRound(1.255,2));//1.25不正确(应为1.26)//测试边缘案例console.log(naiveRound(1.005,2));//1不正确(应为1.01)console.log(naiveRound(2.175,2));//2.17不正确(应为2.18)console.log(naiveRound(5.015,2));//5.01不正确(应为5.02)
为了确定舍入操作是否涉及中点值,Round函数将要舍入的原始值乘以10**n,其中n是返回值中所需的小数位数,然后确定该值的剩余小数部分是否大于或等于.5。由于浮点格式在二进制表示和精度方面存在问题,这种“精确相等测试”对于浮点值是有问题的。这意味着一个数字的任何小数部分如果稍微小于.5(因为精度损失),都不会向上舍入。
在上一个示例中,如果要舍入到两位小数,则5.015是一个中间值,5.015*100的值实际上是501.49999999999994。因为.49999999999994小于.5,所以向下舍入为501,最终结果为5.01。
更好的实施
指数表示法
通过将数字转换为指数表示法中的字符串,正数将按预期取整。但是,请注意负数与正数的舍入方式不同。
事实上,它执行的基本上等同于“向上舍入一半”的操作。作为规则,您将看到,尽管舍入(1.005,2)的值为1.01,但舍入(-1.005,2)仍计算为-1。lodash-round方法使用了这种技术。
/***向上舍入一半(“向正无穷大舍入一半”)*负数的舍入方式不同于正数。*/函数舍入(num,decimalPlaces=0){num=数学舍入(num+“e”+小数位数);返回数字(num+“e”+-decimalPlaces);}//一半的测试舍入console.log(圆形(0.5));//1.console.log(圆形(-0.5));//0//测试边缘案例console.log(圆形(1.005,2));//1.01console.log(圆形(2.175,2));//2.18console.log(圆形(5.015,2));//5.02console.log(圆形(-1.005,2));//-1.console.log(圆形(-2.175,2));//-2.17console.log(圆形(-5.015,2));//-5.01
如果您想要负数舍入时的通常行为,则需要在调用Math.rround()之前将负数转换为正数,然后在返回之前将它们转换回负数。
// Round half away from zero
function round(num, decimalPlaces = 0) {
if (num < 0)
return -round(-num, decimalPlaces);
num = Math.round(num + "e" + decimalPlaces);
return Number(num + "e" + -decimalPlaces);
}
近似舍入
为了纠正上一个naiveRound示例中显示的舍入问题,我们可以定义一个自定义舍入函数,该函数执行“近似相等”测试,以确定分数值是否足够接近中点值以进行中点舍入。
//离零约一半函数舍入(num,decimalPlaces=0){如果(num<0)return-round(-num,decimalPlaces);var p=数学.pow(10,小数位数);变量n=num*p;var f=n-数学楼层(n);var e=数字.EPSILON*n;//确定该分数是否为中点值。返回(f>=.5-e)?数学ceil(n)/p:数学floor(n)/p;}//一半的测试舍入console.log(圆形(0.5));//1.console.log(圆形(-0.5));//-1.//测试边缘案例console.log(圆形(1.005,2));//1.01console.log(圆形(2.175,2));//2.18console.log(圆形(5.015,2));//5.02console.log(圆形(-1.005,2));//-1.01console.log(圆形(-2.175,2));//-2.18console.log(圆形(-5.015,2));//-5.02
数字.EPSILON
有一种不同的纯数学技术来执行最接近的舍入(使用“距离零的舍入半”),其中在调用舍入函数之前应用epsilon校正。
简单地说,我们在舍入之前将最小的浮点值(=1.0 ulp;单位在最后一位)添加到乘积中。这将移动到下一个可表示的浮点值,远离零,因此它将抵消在乘以10**n期间可能出现的二进制舍入误差。
/***从零开始舍入一半(“商业”舍入)*使用校正来抵消浮点精度。*对正数和负数对称工作。*/函数舍入(num,decimalPlaces=0){var p=数学.pow(10,小数位数);var n=(num*p)*(1+Number.EPSILON);return数学舍入(n)/p;}//一半舍入console.log(圆形(0.5));//1.console.log(圆形(-0.5));//-1.//测试边缘案例console.log(圆形(1.005,2));//1.01console.log(圆形(2.175,2));//2.18console.log(圆形(5.015,2));//5.02console.log(圆形(-1.005,2));//-1.01console.log(圆形(-2.175,2));//-2.18console.log(圆形(-5.015,2));//-5.02
添加1 ulp后,5.015*100的值(即501.49999999999994)将被修正为501.50000000000006,这将四舍五入到502,最终结果为5.02。
请注意,最后一位单位的大小(“ulp”)由(1)数字的大小和(2)相对机器ε(2^-52)决定。Ulps在震级较大的数值上比在震级较小的数值上相对较大。
双舍入
这里,我们使用toPrecision()方法去除中间计算中的浮点舍入错误。简单地说,我们四舍五入到15位有效数字,以去除第16位有效数字的舍入误差。PHP 7 round函数也使用这种将结果预转为有效数字的技术。
5.015*100的值(即501.49999999999994)将首先四舍五入到15位有效数字,即501.500000000000,然后再次四舍五进到502,最终结果为5.02。
//距离零的一半函数舍入(num,decimalPlaces=0){如果(num<0)return-round(-num,decimalPlaces);var p=数学.pow(10,小数位数);var n=(num*p).toPrecision(15);return数学舍入(n)/p;}//一半舍入console.log(圆形(0.5));//1.console.log(圆形(-0.5));//-1.//测试边缘案例console.log(圆形(1.005,2));//1.01console.log(圆形(2.175,2));//2.18console.log(圆形(5.015,2));//5.02console.log(圆形(-1.005,2));//-1.01console.log(圆形(-2.175,2));//-2.18console.log(圆形(-5.015,2));//-5.02
任意精度JavaScript库-decimal.js
//距离零的一半函数舍入(num,decimalPlaces=0){return new Decimal(num).toDecimalPlaces(decimalPlaces).toNumber();}//一半舍入console.log(圆形(0.5));//1.console.log(圆形(-0.5));//-1.//测试边缘案例console.log(圆形(1.005,2));//1.01console.log(圆形(2.175,2));//2.18console.log(圆形(5.015,2));//5.02console.log(圆形(-1.005,2));//-1.01console.log(圆形(-2.175,2));//-2.18console.log(圆形(-5.015,2));//-5.02<script src=“https://cdnjs.cloudflare.com/ajax/libs/decimal.js/10.2.1/decimal.js“integrity=”sha512-GKse2KVGCCMVBn4riigHjXE8j5hCxYLPXDw8avjUtrt+a9TbZFtIKGdArXwYOlZvdmkhQLWQ46ZE3Q1RIa7uQ=“crossrorigin=”匿名“></script>
解决方案1:以指数表示的字符串
灵感来自KFish提供的解决方案:https://stackoverflow.com/a/55521592/4208440
一种简单的插入式解决方案,可提供精确的小数舍入、地板和上限,以达到特定的小数位数,而无需添加整个库。它通过修复二进制舍入问题,将浮点值处理得更像小数,以避免意外结果:例如,floor((0.1+0.7)*10)将返回预期结果8。
数字四舍五入到特定的小数位数。指定负精度将舍入到小数点左侧的任意位数。
//解决方案1var DecimalPrecision=(函数){if(Math.trunc==未定义){Math.trunc=函数(v){返回v<0?数学ceil(v):数学floor(v);};}var decimalAdjust=函数本身(type,num,decimalPlaces){if(类型==“round”&&num<0)return-我自己(type,-num,decimalPlaces);var shift=函数(值,指数){值=(值+'e').拆分(e');返回+(值[0]+'e'+(+值[1]+(指数||0)));};var n=移位(num,+小数位数);返回移位(数学[type](n),-decimalPlaces);};返回{//十进制舍入(距离零的一半)round:函数(num,decimalPlaces){return decimalAdjust('round',num,decimalPlaces);},//十进制ceilceil:函数(num,decimalPlaces){return decimalAdjust('eil',num,decimalPlaces);},//十进制楼层floor:函数(num,decimalPlaces){return decimalAdjust('floor',num,decimalPlaces);},//十进制截断trunca:函数(num,decimalPlaces){return decimalAdjust('trunc',num,decimalPlaces);},//使用定点表示法格式化toFixed:函数(num,decimalPlaces){return decimalAdjust('round',num,decimalPlaces).toFixed(decimalPlace);}};})();//一半的测试舍入console.log(DecimalPrecision.round(0.5));//1.console.log(DecimalPrecision.round(-0.5));//-1.//测试非常小的数字console.log(DecimalPrecision.ceil(1e-8,2)==0.01);console.log(DecimalPrecision.floor(1e-8,2)==0);//测试简单案例console.log(DecimalPrecision.round(5.12,1)==5.1);console.log(DecimalPrecision.round(-5.12,1)==-5.1);console.log(DecimalPrecision.ceil(5.12,1)==5.2);console.log(DecimalPrecision.ceil(-5.12,1)==-5.1);console.log(DecimalPrecision.floor(5.12,1)===5.1);console.log(DecimalPrecision.floor(-5.12,1)==-5.2);console.log(DecimalPrecision.trunc(5.12,1)==5.1);console.log(DecimalPrecision.trunc(-5.12,1)==-5.1);//测试圆形边壳console.log(DecimalPrecision.round(1.005,2)==1.01);console.log(DecimalPrecision.round(39.425,2)==39.43);console.log(DecimalPrecision.round(-1.005,2)==-1.01);console.log(DecimalPrecision.round(-39.425,2)==-39.43);//测试ceil的边缘案例console.log(DecimalPrecision.ceil(9.13,2)==9.13);console.log(DecimalPrecision.ceil(65.18,2)==65.18);console.log(DecimalPrecision.ceil(-2.26,2)==-2.26);console.log(DecimalPrecision.ceil(-18.15,2)==-18.15);//测试地板边缘案例console.log(DecimalPrecision.floor(2.26,2)==2.26);console.log(DecimalPrecision.floor(18.15,2)==18.15);console.log(DecimalPrecision.floor(-9.13,2)==-9.13);console.log(DecimalPrecision.floor(-65.18,2)==-65.18);//trunc的边缘用例测试console.log(DecimalPrecision.trunc(2.26,2)==2.26);console.log(DecimalPrecision.trunc(18.15,2)==18.15);console.log(DecimalPrecision.trunc(-2.26,2)==-2.26);console.log(DecimalPrecision.trunc(-18.15,2)==-18.15);//测试到数十和数百console.log(DecimalPrecision.round(1262.48,-1)==1260);console.log(DecimalPrecision.round(1262.48,-2)==1300);//测试到Fixed()console.log(DecimalPrecision.toFixed(1.005,2)==“1.01”);
解决方案2:纯数学(编号:EPSILON)
由于性能原因,此解决方案避免了任何类型的字符串转换/操作。
//解决方案2var DecimalPrecision2=(函数){if(数字.EPSILON===未定义){Number.EPSILON=数学功率(2,-52);}if(数学符号==未定义){Math.sign=函数(x){return((x>0)-(x<0))||+x;};}返回{//十进制舍入(距离零的一半)round:函数(num,decimalPlaces){var p=数学.pow(10,小数位数||0);var n=(num*p)*(1+Number.EPSILON);return数学舍入(n)/p;},//十进制ceilceil:函数(num,decimalPlaces){var p=数学.pow(10,小数位数||0);var n=(num*p)*(1-数学符号(num)*数字.EPSILON);返回数学ceil(n)/p;},//十进制楼层floor:函数(num,decimalPlaces){var p=数学.pow(10,小数位数||0);var n=(num*p)*(1+数学符号(num)*数字.EPSILON);返回数学楼层(n)/p;},//十进制截断trunca:函数(num,decimalPlaces){return(num<0?this.eil:this.floor)(num,decimalPlaces);},//使用定点表示法格式化toFixed:函数(num,decimalPlaces){返回this.round(num,decimalPlaces).toFixed(decimalPlace);}};})();//一半的测试舍入console.log(DecimalPrecision2.round(0.5));//1.console.log(DecimalPrecision2.round(-0.5));//-1.//测试非常小的数字console.log(DecimalPrecision2.ceil(1e-8,2)==0.01);console.log(DecimalPrecision2.floor(1e-8,2)==0);//测试简单案例console.log(DecimalPrecision2.round(5.12,1)==5.1);console.log(DecimalPrecision2.round(-5.12,1)==-5.1);console.log(DecimalPrecision2.ceil(5.12,1)==5.2);console.log(DecimalPrecision2.ceil(-5.12,1)==-5.1);console.log(DecimalPrecision2.floor(5.12,1)==5.1);console.log(DecimalPrecision2.floor(-5.12,1)==-5.2);console.log(DecimalPrecision2.trunc(5.12,1)==5.1);console.log(DecimalPrecision2.trunc(-5.12,1)==-5.1);//测试圆形边壳console.log(DecimalPrecision2.round(1.005,2)==1.01);console.log(DecimalPrecision2.round(39.425,2)==39.43);console.log(DecimalPrecision2.round(-1.005,2)==-1.01);console.log(DecimalPrecision2.round(-39.425,2)==-39.43);//测试ceil的边缘案例console.log(DecimalPrecision2.ceil(9.13,2)==9.13);console.log(DecimalPrecision2.ceil(65.18,2)==65.18);console.log(DecimalPrecision2.ceil(-2.26,2)==-2.26);console.log(DecimalPrecision2.ceil(-18.15,2)==-18.15);//测试地板边缘案例console.log(DecimalPrecision2.floor(2.26,2)==2.26);console.log(DecimalPrecision2.floor(18.15,2)==18.15);console.log(DecimalPrecision2.floor(-9.13,2)==-9.13);console.log(DecimalPrecision2.floor(-65.18,2)==-65.18);//trunc的边缘用例测试console.log(DecimalPrecision2.trunc(2.26,2)==2.26);console.log(DecimalPrecision2.trunc(18.15,2)==18.15);console.log(DecimalPrecision2.trunc(-2.26,2)==-2.26);console.log(DecimalPrecision2.trunc(-18.15,2)==-18.15);//测试到数十和数百console.log(DecimalPrecision2.round(1262.48,-1)==1260);console.log(DecimalPrecision2.round(1262.48,-2)==1300);//测试到Fixed()console.log(DecimalPrecision2.toFixed(1.005,2)==“1.01”);
解决方案3:双舍入
此解决方案使用toPrecision()方法去除浮点舍入错误。
//解决方案3var DecimalPrecision3=(函数){if(Math.trunc==未定义){Math.trunc=函数(v){返回v<0?数学ceil(v):数学floor(v);};}var功率=[1e0、1e1、1e2、1e3、1e4、1e5、1e6、1e7,1e8、1e9、1e10、1e11、1e12、1e13、1e14、1e15,1e16、1e17、1e18、1e19、1e20、1e21、1e22];var intpow10=函数(功率){/*不在查找表中*/如果(功率<0 | |功率>22){return Math.pow(10,幂);}返回功率[功率];};//消除二进制浮点精度。var stripError=函数(num){if(数字.isInteger(num))返回num;返回parseFloat(num.toPrecision(15));};var decimalAdjust=函数本身(type,num,decimalPlaces){if(类型==“round”&&num<0)return-我自己(type,-num,decimalPlaces);var p=intpow10(小数位数||0);var n=stripError(num*p);返回数学[type](n)/p;};返回{//十进制舍入(距离零的一半)round:函数(num,decimalPlaces){return decimalAdjust('round',num,decimalPlaces);},//十进制ceilceil:函数(num,decimalPlaces){return decimalAdjust('eil',num,decimalPlaces);},//十进制楼层floor:函数(num,decimalPlaces){return decimalAdjust('floor',num,decimalPlaces);},//十进制截断trunca:函数(num,decimalPlaces){return decimalAdjust('trunc',num,decimalPlaces);},//使用定点表示法格式化toFixed:函数(num,decimalPlaces){return decimalAdjust('round',num,decimalPlaces).toFixed(decimalPlace);}};})();//一半的测试舍入console.log(DecimalPrecision3.round(0.5));//1.console.log(DecimalPrecision3.round(-0.5));//-1.//测试非常小的数字console.log(DecimalPrecision3.ceil(1e-8,2)==0.01);console.log(DecimalPrecision3.floor(1e-8,2)==0);//测试简单案例console.log(DecimalPrecision3.round(5.12,1)==5.1);console.log(DecimalPrecision3.round(-5.12,1)==-5.1);console.log(DecimalPrecision3.ceil(5.12,1)==5.2);console.log(DecimalPrecision3.ceil(-5.12,1)==-5.1);console.log(DecimalPrecision3.floor(5.12,1)==5.1);console.log(DecimalPrecision3.floor(-5.12,1)==-5.2);console.log(DecimalPrecision3.trunc(5.12,1)==5.1);console.log(DecimalPrecision3.trunc(-5.12,1)==-5.1);//测试圆形边壳console.log(DecimalPrecision3.round(1.005,2)==1.01);console.log(DecimalPrecision3.round(39.425,2)==39.43);console.log(DecimalPrecision3.round(-1.005,2)==-1.01);console.log(DecimalPrecision3.round(-39.425,2)==-39.43);//测试ceil的边缘案例console.log(DecimalPrecision3.ceil(9.13,2)==9.13);console.log(DecimalPrecision3.ceil(65.18,2)==65.18);console.log(DecimalPrecision3.ceil(-2.26,2)==-2.26);console.log(DecimalPrecision3.ceil(-18.15,2)==-18.15);//测试地板边缘案例console.log(DecimalPrecision3.floor(2.26,2)==2.26);console.log(DecimalPrecision3.floor(18.15,2)==18.15);console.log(DecimalPrecision3.floor(-9.13,2)==-9.13);console.log(DecimalPrecision3.floor(-65.18,2)==-65.18);//trunc的边缘用例测试console.log(DecimalPrecision3.trunc(2.26,2)==2.26);console.log(DecimalPrecision3.trunc(18.15,2)==18.15);console.log(DecimalPrecision3.trunc(-2.26,2)==-2.26);console.log(DecimalPrecision3.trunc(-18.15,2)==-18.15);//测试到数十和数百console.log(DecimalPrecision3.round(1262.48,-1)==1260);console.log(DecimalPrecision3.round(1262.48,-2)==1300);//测试到Fixed()console.log(DecimalPrecision3.toFixed(1.005,2)==“1.01”);
解决方案4:双舍入v2
此解决方案与解决方案3类似,但它使用了自定义的toPrecision()函数。
//解决方案4var DecimalPrecision4=(函数){if(Math.trunc==未定义){Math.trunc=函数(v){返回v<0?数学ceil(v):数学floor(v);};}var功率=[1e0、1e1、1e2、1e3、1e4、1e5、1e6、1e7,1e8、1e9、1e10、1e11、1e12、1e13、1e14、1e15,1e16、1e17、1e18、1e19、1e20、1e21、1e22];var intpow10=函数(功率){/*不在查找表中*/如果(功率<0 | |功率>22){return Math.pow(10,幂);}返回功率[功率];};var toPrecision=函数(num,significantDigits){//提前返回±0、NaN和Infinity。if(!num||!Number.isFinite(num))返回num;//计算小数点的移位(sf-leftSidedDigits)。var shift=significantDigits-1-数学楼层(数学log10(数学abs(num)));//如果舍入到相同或更高的精度,则返回。var decimalPlaces=0;for(var p=1;num!=数学舍入(num*p)/p;p*=10)小数位数++;if(shift>=小数位数)返回num;//舍入为“移位”小数位数var scale=intpow10(数学.abs(移位));返回移位>0?数学舍入(num*刻度)/刻度:数学舍入(num/scale)*刻度;};//消除二进制浮点精度。var stripError=函数(num){if(数字.isInteger(num))返回num;返回精度(num,15);};var decimalAdjust=函数本身(type,num,decimalPlaces){if(类型==“round”&&num<0)return-我自己(type,-num,decimalPlaces);var p=intpow10(小数位数||0);var n=stripError(num*p);返回数学[type](n)/p;};返回{//十进制舍入(距离零的一半)round:函数(num,decimalPlaces){return decimalAdjust('round',num,decimalPlaces);},//十进制ceilceil:函数(num,decimalPlaces){return decimalAdjust('eil',num,decimalPlaces);},//十进制楼层floor:函数(num,decimalPlaces){return decimalAdjust('floor',num,decimalPlaces);},//十进制截断trunca:函数(num,decimalPlaces){return decimalAdjust('trunc',num,decimalPlaces);},//使用定点表示法格式化toFixed:函数(num,decimalPlaces){return decimalAdjust('round',num,decimalPlaces).toFixed(decimalPlace);}};})();//一半的测试舍入console.log(DecimalPrecision4.round(0.5));//1.console.log(DecimalPrecision4.round(-0.5));//-1.//测试非常小的数字console.log(DecimalPrecision4.ceil(1e-8,2)==0.01);console.log(DecimalPrecision4.floor(1e-8,2)==0);//测试简单案例console.log(DecimalPrecision4.round(5.12,1)==5.1);console.log(DecimalPrecision4.round(-5.12,1)==-5.1);console.log(DecimalPrecision4.ceil(5.12,1)==5.2);console.log(DecimalPrecision4.ceil(-5.12,1)==-5.1);console.log(DecimalPrecision4.floor(5.12,1)==5.1);console.log(DecimalPrecision4.floor(-5.12,1)==-5.2);console.log(DecimalPrecision4.trunc(5.12,1)==5.1);console.log(DecimalPrecision4.trunc(-5.12,1)==-5.1);//测试圆形边壳console.log(DecimalPrecision4.round(1.005,2)==1.01);console.log(DecimalPrecision4.round(39.425,2)==39.43);console.log(DecimalPrecision4.round(-1.005,2)==-1.01);console.log(DecimalPrecision4.round(-39.425,2)==-39.43);//测试ceil的边缘案例console.log(DecimalPrecision4.ceil(9.13,2)==9.13);console.log(DecimalPrecision4.ceil(65.18,2)==65.18);console.log(DecimalPrecision4.ceil(-2.26,2)==-2.26);console.log(DecimalPrecision4.ceil(-18.15,2)==-18.15);//测试地板边缘案例console.log(DecimalPrecision4.floor(2.26,2)==2.26);console.log(DecimalPrecision4.floor(18.15,2)==18.15);console.log(DecimalPrecision4.floor(-9.13,2)==-9.13);console.log(DecimalPrecision4.floor(-65.18,2)==-65.18);//trunc的边缘用例测试console.log(DecimalPrecision4.trunc(2.26,2)==2.26);console.log(DecimalPrecision4.trunc(18.15,2)==18.15);console.log(DecimalPrecision4.trunc(-2.26,2)==-2.26);console.log(DecimalPrecision4.trunc(-18.15,2)==-18.15);//测试到数十和数百console.log(DecimalPrecision4.round(1262.48,-1)==1260);console.log(DecimalPrecision4.round(1262.48,-2)==1300);//测试到Fixed()console.log(DecimalPrecision4.toFixed(1.005,2)==“1.01”);
基准
http://jsbench.github.io/#31ec3a8b3d22bd840f8e6822e681a3ac
下面是一个比较Chrome109.0.0.0上上述解决方案每秒操作数的基准。使用Number.EPSILON的舍入功能至少快10x-20x。显然,所有浏览器都不同,因此您的里程可能会有所不同。
(注:越多越好)
感谢@Mike添加基准的截图。
其他回答
最简单的方法是使用toFixed,然后使用Number函数去除尾随零:
const number = 15.5;
Number(number.toFixed(2)); // 15.5
const number = 1.7777777;
Number(number.toFixed(2)); // 1.78
它可能对你有用,
Math.round(num * 100)/100;
了解toFixed和round之间的区别。您可以查看Math.round(num)vs num.toFixed(0)和浏览器不一致性。
如果使用的是Lodash库,可以使用Lodash的舍入方法,如下所示。
_.round(number, precision)
例如:
_.round(1.7777777, 2) = 1.78
如果值是文本类型:
parseFloat("123.456").toFixed(2);
如果值是数字:
var numb = 123.23454;
numb = numb.toFixed(2);
有一个缺点,像1.5这样的值将给出“1.50”作为输出。@minitech建议的修复方法:
var numb = 1.5;
numb = +numb.toFixed(2);
// Note the plus sign that drops any "extra" zeroes at the end.
// It changes the result (which is a string) into a number again (think "0 + foo"),
// which means that it uses only as many digits as necessary.
Math.round似乎是一个更好的解决方案。但事实并非如此!在某些情况下,它不会正确舍入:
Math.round(1.005 * 100)/100 // Returns 1 instead of expected 1.01!
toFixed()在某些情况下也不会正确舍入(在Chrome v.55.0.2883.87中测试)!
示例:
parseFloat("1.555").toFixed(2); // Returns 1.55 instead of 1.56.
parseFloat("1.5550").toFixed(2); // Returns 1.55 instead of 1.56.
// However, it will return correct result if you round 1.5551.
parseFloat("1.5551").toFixed(2); // Returns 1.56 as expected.
1.3555.toFixed(3) // Returns 1.355 instead of expected 1.356.
// However, it will return correct result if you round 1.35551.
1.35551.toFixed(2); // Returns 1.36 as expected.
我想,这是因为1.555实际上就像是幕后的浮球1.55499994。
解决方案1是使用具有所需舍入算法的脚本,例如:
function roundNumber(num, scale) {
if(!("" + num).includes("e")) {
return +(Math.round(num + "e+" + scale) + "e-" + scale);
} else {
var arr = ("" + num).split("e");
var sig = ""
if(+arr[1] + scale > 0) {
sig = "+";
}
return +(Math.round(+arr[0] + "e" + sig + (+arr[1] + scale)) + "e-" + scale);
}
}
它也在Plunker。
注意:这并不是每个人都能通用的解决方案。有几种不同的舍入算法。您的实现可能不同,这取决于您的需求。请参见舍入。
解决方案2是避免前端计算,并从后端服务器提取舍入值。
另一种可能的解决方案,也不是防弹的。
Math.round((num + Number.EPSILON) * 100) / 100
在某些情况下,当您舍入像1.3549999999999998这样的数字时,它将返回错误的结果。它应该是1.35,但结果是1.36。
这个问题很复杂。
假设我们有一个函数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);
}