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

输入:

10
1.7777777
9.1

输出:

10
1.78
9.1

如何在JavaScript中执行此操作?


当前回答

将类型保留为整数,以便以后进行排序或其他算术运算:

Math.round(1.7777777 * 100)/100

1.78

// Round up!
Math.ceil(1.7777777 * 100)/100

1.78

// Round down!
Math.floor(1.7777777 * 100)/100

1.77

或转换为字符串:

(1.7777777).toFixed(2)

"1.77"

其他回答

如果值是文本类型:

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。

避免舍入到任意位数的二进制问题的适当方法是:

function roundToDigits(number, digits) {
  return Number(Math.round(Number(number + 'e' + digits)) + 'e-' + digits);
}

修复toFixed()函数的一种方法是:

Number.prototype.toFixed = (prototype => {
    const toFixed = prototype.toFixed;

    // noinspection JSVoidFunctionReturnValueUsed
    return function (fractionDigits) {
        if (!fractionDigits) {
            return toFixed.call(this);
        } else {
            // Avoid binary rounding issues
            fractionDigits = Math.floor(fractionDigits);
            const n = Number(Math.round(Number(+this + 'e' + fractionDigits)) + 'e-' + fractionDigits);
            return toFixed.call(n, fractionDigits);
        }
    };
})(Number.prototype);

可以使用.toFixed(小数位数)。

var str = 10.234.toFixed(2); // => '10.23'
var number = Number(str); // => 10.23

如果您碰巧已经在使用D3.js库,那么他们有一个强大的数字格式库。

舍入具体为D3舍入。

在您的案例中,答案是:

> d3.round(1.777777, 2)
1.78

> d3.round(1.7, 2)
1.7

> d3.round(1, 2)
1

这个问题很复杂。

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