我想最多四舍五入两位小数,但只有在必要时。

输入:

10
1.7777777
9.1

输出:

10
1.78
9.1

如何在JavaScript中执行此操作?


当前回答

您应该使用:

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),因为我想要一个足够宽松的等式检查器来累积浮点错误。

到目前为止,它看起来很适合我。我希望这会有所帮助。

其他回答

这里有一个简单的方法:

Math.round(value * 100) / 100

不过,您可能需要继续创建一个单独的函数来为您执行此操作:

function roundToTwo(value) {
    return(Math.round(value * 100) / 100);
}

然后,只需传入值。

通过添加第二个参数,可以将其增强为任意小数位数。

function myRound(value, places) {
    var multiplier = Math.pow(10, places);

    return (Math.round(value * multiplier) / multiplier);
}

这对我(TypeScript)起到了作用:

round(decimal: number, decimalPoints: number): number{
    let roundedValue = Math.round(decimal * Math.pow(10, decimalPoints)) / Math.pow(10, decimalPoints);

    console.log(`Rounded ${decimal} to ${roundedValue}`);
    return roundedValue;
}

样本输出

Rounded 18.339840000000436 to 18.34
Rounded 52.48283999999984 to 52.48
Rounded 57.24612000000036 to 57.25
Rounded 23.068320000000142 to 23.07
Rounded 7.792980000000398 to 7.79
Rounded 31.54157999999981 to 31.54
Rounded 36.79686000000004 to 36.8
Rounded 34.723080000000124 to 34.72
Rounded 8.4375 to 8.44
Rounded 15.666960000000074 to 15.67
Rounded 29.531279999999924 to 29.53
Rounded 8.277420000000006 to 8.28

尝试此轻量级解决方案:

function round(x, digits){
  return parseFloat(x.toFixed(digits))
}

 round(1.222,  2);
 // 1.22
 round(1.222, 10);
 // 1.222

这个问题很复杂。

假设我们有一个函数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);
}

请参阅AmrAli的答案,以了解此解决方案的所有不同调整的更全面的运行和性能细分。

var DecimalPrecision=(函数){if(数字.EPSILON===未定义){Number.EPSILON=数学功率(2,-52);}if(Number.isInteger==未定义){Number.isInteger=函数(值){返回值类型==“number”&&isFinite(值)&&数学下限(值)==值;};}this.isRound=函数(n,p){设l=n.toString().split('.')[1].length;返回(p>=l);}this.round=函数(n,p=2){if(Number.isInteger(n)|| this.isRound(n,p))返回n;设r=0.5*Number.EPSILON*n;设o=1;而(p-->0)o*=10;如果(n<0)o*=-1;返回数学舍入((n+r)*o)/o;}this.ceil=函数(n,p=2){if(Number.isInteger(n)|| this.isRound(n,p))返回n;设r=0.5*Number.EPSILON*n;设o=1;而(p-->0)o*=10;返回Math.ceil((n+r)*o)/o;}this.flor=函数(n,p=2){if(Number.isInteger(n)|| this.isRound(n,p))返回n;设r=0.5*Number.EPSILON*n;设o=1;而(p-->0)o*=10;返回数学楼层((n+r)*o)/o;}返回此;})();console.log(DecimalPrecision.round(1.005));console.log(DecimalPrecision.ceil(1.005));console.log(DecimalPrecision.floor(1.005));console.log(DecimalPrecision.round(1.0049999));console.log(DecimalPrecision.ceil(1.0049999));console.log(DecimalPrecision.floor(1.0049999));console.log(DecimalPrecision.round(2.175495134384,7));console.log(DecimalPrecision.round(2.1753543549,8));console.log(DecimalPrecision.round(2.1755465135333,4));console.log(DecimalPrecision.ceil(17,4));console.log(DecimalPrecision.ceil(17.1,4));console.log(DecimalPrecision.ceil(17.1,15));