Esta pregunta es complicada.
Supongamos que tenemos una función, roundTo2DP(num)
que toma un flotador como argumento y devuelve un valor redondeado a 2 decimales. ¿Qué debe evaluar cada una de estas expresiones?
roundTo2DP(0.014999999999999999)
roundTo2DP(0.0150000000000000001)
roundTo2DP(0.015)
La respuesta 'obvia' es que el primer ejemplo debería redondear a 0.01 (porque está más cerca de 0.01 que a 0.02) mientras que los otros dos deberían redondear a 0.02 (porque 0.0150000000000000001 está más cerca de 0.02 que a 0.01, y porque 0.015 está exactamente a mitad de camino entre ellos y hay una convención matemática de que tales números se redondean).
El problema, que puede haber adivinado, es que roundTo2DP
no se puede implementar para dar esas respuestas obvias, porque los tres números que se le pasan son el mismo número . Los números de punto flotante binario IEEE 754 (el tipo utilizado por JavaScript) no pueden representar exactamente la mayoría de los números no enteros, por lo que los tres literales numéricos anteriores se redondean a un número de punto flotante válido cercano. Este número, como sucede, es exactamente
0.01499999999999999944488848768742172978818416595458984375
que está más cerca de 0.01 que de 0.02.
Puede ver que los tres números son iguales en la consola de su navegador, en el shell de nodo u otro intérprete de JavaScript. Solo compáralos:
> 0.014999999999999999 === 0.0150000000000000001
true
Entonces, cuando escribo m = 0.0150000000000000001
, el valor exacto con elm
que termino está más cerca de 0.01
lo que está 0.02
. Y sin embargo, si me convierto m
en una cadena ...
> var m = 0.0150000000000000001;
> console.log(String(m));
0.015
> var m = 0.014999999999999999;
> console.log(String(m));
0.015
... obtengo 0.015, que debería redondear a 0.02, y que notablemente no es el número de 56 decimales que dije anteriormente que todos estos números eran exactamente iguales. Entonces, ¿qué magia oscura es esta?
La respuesta se puede encontrar en la especificación ECMAScript, en la sección 7.1.12.1: ToString aplicado al tipo Number . Aquí se establecen las reglas para convertir un número m en una cadena. La parte clave es el punto 5, en el que se genera un número entero s cuyos dígitos se utilizarán en la representación de cadena de m :
supongamos que n , k y s son números enteros tales que k ≥ 1, 10 k -1 ≤ s <10 k , el valor de Número para s × 10 n - k es m , y k es lo más pequeño posible. Tenga en cuenta que k es el número de dígitos en la representación decimal de s , que s no es divisible por 10, y que el dígito menos significativo de s no está necesariamente determinado únicamente por estos criterios.
La parte clave aquí es el requisito de que " k es lo más pequeño posible". Lo que equivale a ese requisito es un requisito que, dado un Número m
, el valor de String(m)
debe tener el menor número posible de dígitos sin dejar de cumplir el requisito de que Number(String(m)) === m
. Como ya sabemos eso 0.015 === 0.0150000000000000001
, ahora está claro por quéString(0.0150000000000000001) === '0.015'
debe ser cierto.
Por supuesto, nada de esta discusión ha respondido directamente lo que roundTo2DP(m)
debería regresar. Si m
el valor exacto es 0.01499999999999999944488848768742172978818416595458984375, pero su representación de cadena es '0.015', entonces cuál es el correcto respuesta , matemática, práctica, filosófica o lo que sea, cuando redondeamos a dos decimales?
No hay una única respuesta correcta a esto. Depende de su caso de uso. Probablemente desee respetar la representación de cadena y redondear hacia arriba cuando:
- El valor que se representa es intrínsecamente discreto, por ejemplo, una cantidad de moneda en una moneda de 3 decimales, como los dinares. En este caso, el valor verdadero de un Número como 0.015 es 0.015, y la representación 0.0149999999 ... que obtiene en coma flotante binaria es un error de redondeo. (Por supuesto, muchos argumentarán, razonablemente, que debe usar una biblioteca decimal para manejar dichos valores y nunca representarlos como números binarios de coma flotante en primer lugar).
- El valor fue escrito por un usuario. En este caso, nuevamente, el número decimal exacto ingresado es más 'verdadero' que la representación de punto flotante binario más cercano.
Por otro lado, es probable que desee respetar el valor de punto flotante binario y redondear hacia abajo cuando su valor proviene de una escala inherentemente continua, por ejemplo, si es una lectura de un sensor.
Estos dos enfoques requieren un código diferente. Para respetar la representación de la cadena del número, podemos (con un poco de código razonablemente sutil) implementar nuestro propio redondeo que actúa directamente sobre la representación de la cadena, dígito a dígito, utilizando el mismo algoritmo que hubiera utilizado en la escuela cuando se les enseñó a redondear números. A continuación se muestra un ejemplo que respeta el requisito del OP de representar el número a 2 decimales "solo cuando sea necesario" quitando los ceros finales después del punto decimal; puede, por supuesto, necesitar ajustarlo a sus necesidades precisas.
/**
* 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;
}
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.
finalNumber = Number(beforePoint)+1;
} 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+$/, '')
}
Ejemplo de uso:
> 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'
La función anterior es probablemente lo que desea usar para evitar que los usuarios sean testigos de que los números que ingresaron se redondearon incorrectamente.
(Como alternativa, también puede probar la biblioteca round10 que proporciona una función de comportamiento similar con una implementación muy diferente).
Pero, ¿qué sucede si tiene el segundo tipo de Número, un valor tomado de una escala continua, donde no hay razón para pensar que las representaciones decimales aproximadas con menos decimales son más precisas que las que tienen más? En ese caso, no queremos respetar la representación de cadena, porque esa representación (como se explica en la especificación) ya está redondeada; no queremos cometer el error de decir "0.014999999 ... 375 redondea a 0.015, que redondea a 0.02, entonces 0.014999999 ... 375 redondea a 0.02".
Aquí podemos simplemente usar el toFixed
método incorporado . Tenga en cuenta que al invocar Number()
la Cadena devuelta por toFixed
, obtenemos un Número cuya representación de Cadena no tiene ceros finales (gracias a la forma en que JavaScript calcula la representación de Cadena de un Número, discutido anteriormente en esta respuesta).
/**
* 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);
}