7

Me gustaría redondear a dos decimales, sólo cuando sea necesario. A continuación ejemplos de entradas y salidas

Entrada:

10
1.7777777
9.1

Salida:

10
1.78
9.1

¿Cómo puedo hacer esto en JavaScript?

Fragmento

var valor = [
  10,
  1.77777777,
  9.1
];
var resultado = valor.map(Math.round);
console.log(resultado);

Pregunta inspirada por Round to at most 2 decimal places

Rubén
  • 10,857
  • 6
  • 35
  • 79
  • Haslo directamente en tu Base de datos, acostumbrate a usar procedimientos en cada uno de tus aplicaciones te facilitan la vida. en sql server usa round() – PieroDev Feb 11 '17 at 14:46
  • 2
    @Pierro: Gracias por el comentario, pero no se trata de una de base de datos y tampoco se trata sobre sql. Me la impresión de que sólo leíste el título ¿estoy en lo correcto? – Rubén Feb 11 '17 at 17:04
  • asociación: https://stackoverflow.com/q/11832914/1595451 – Rubén Mar 02 '19 at 16:00

8 Answers8

8

También se puede usar el método toFixed(), aunque convierte el numero en una string y habría que utilizar la función Number():

var numero = 1.77777777;
numero = Number(numero.toFixed(2));
console.log(numero); // Muestra 1.78

Lo malo es que deja 0 si el numero no contiene decimal o es menor al parámetro pasado a toFixed(). Con esta pequeña función se elimina ese problema:

function redondearDecimales(numero, decimales) {
    numeroRegexp = new RegExp('\\d\\.(\\d){' + decimales + ',}');   // Expresion regular para numeros con un cierto numero de decimales o mas
    if (numeroRegexp.test(numero)) {         // Ya que el numero tiene el numero de decimales requeridos o mas, se realiza el redondeo
        return Number(numero.toFixed(decimales));
    } else {
        return Number(numero.toFixed(decimales)) === 0 ? 0 : numero;  // En valores muy bajos, se comprueba si el numero es 0 (con el redondeo deseado), si no lo es se devuelve el numero otra vez.
    }
}



console.log(redondearDecimales(1.777, 2));       // Devuelve 1.78
console.log(redondearDecimales(1, 2));           // Devuelve 1
console.log(redondearDecimales(1.7777, 3));      // Devuelve 1.778
console.log(redondearDecimales(0.0000007, 2));   // Devuelve 0
console.log(redondearDecimales(0.0000007, 7));   // Devuelve 7e-7
console.log(redondearDecimales(-1.3456, 2));     // Devuelve -1.35
Enrique B.
  • 327
  • 1
  • 7
  • ¿Conoces el fragmento HTML,JavaScript,CSS? La pregunta y las otras dos respuestas lo usan. – Rubén Feb 11 '17 at 14:24
  • 2
    Hola Enrique, si solamente usas `toFixed()` a secas y por ejemplo introduces el número **2**, `toFixed()` te devolverá **2.00**, y el pretende que solo se redondee si tiene más de dos decimales – lromeraj Feb 11 '17 at 14:29
  • @Javi tienes razón, he incluido una función que usar en lugar toFixed que solucionaría ese tema, ademas de poder redondearlo al numero de decimales deseado. – Enrique B. Feb 11 '17 at 15:08
  • Parecería ser un poco excesivo utilizar regex para obtener el número de decimales. Además, fallaría con `redondearDecimales(0.0000007,2)` por ejemplo – Mariano Feb 11 '17 at 15:22
  • 1
    @Mariano he corregido el fallo con valores muy bajos. Ahora devuelve 0 si es un valor muy bajo **que al redondearlo daria 0** o devuelve el numero en si si el numero de decimales requerido es el del valor bajo. – Enrique B. Feb 11 '17 at 16:09
  • 3
    @EnriqueB. Fue 1 ejemplo puntual para mostrar que hay casos en los que falla, pero sigue habiendo... Ejemplo: `redondearDecimales(-1.3456, 2)`... Y regex sigue siendo una forma extremadamente poco eficiente de obtener la cantidad de decimales – Mariano Feb 11 '17 at 16:25
  • 1
    @Mariano corregido que no se pudiera redondear negativos. – Enrique B. Feb 11 '17 at 17:17
  • @EnriqueB. `redondearDecimales(9e-8,7)` debería devolver `1e-7`. – Mariano Feb 13 '17 at 23:26
6

Estas dos funciones cumplen con todas las pruebas realizadas.

1. Adaptado de MDN (solución recomendada)

Tomando el código de Redondeo Decimal, adapté la solución para que realice un redondeo aritmético simétrico con números negativos. Es decir que redondee -1.5 ≈ -2.

function round(num, decimales = 2) {
    var signo = (num >= 0 ? 1 : -1);
    num = num * signo;
    if (decimales === 0) //con 0 decimales
        return signo * Math.round(num);
    // round(x * 10 ^ decimales)
    num = num.toString().split('e');
    num = Math.round(+(num[0] + 'e' + (num[1] ? (+num[1] + decimales) : decimales)));
    // x * 10 ^ (-decimales)
    num = num.toString().split('e');
    return signo * (num[0] + 'e' + (num[1] ? (+num[1] - decimales) : -decimales));
}


2. Utilizando Intl.NumberFormat()

La función Intl.NumberFormat([locales[, options]]) permite redondear un número correctamente con código nativo. Sin embargo, como se verá más adelante, esta función acepta opciones sensibles al idioma, haciéndola significativamente más lenta.

function intlRound(numero, decimales = 2, usarComa = false) {
    var opciones = {
        maximumFractionDigits: decimales, 
        useGrouping: false
    };
    usarComa = usarComa ? "es" : "en";
    return new Intl.NumberFormat(usarComa, opciones).format(numero);
}

Devuelve un string con la representación del número, redondeado al máximo de decimales establecido. Si se desea reconvertir en número, aplicar parseFloat() al resultado.



Discusión

Al manipular números realizando operaciones en coma flotante, existe una limitación para representarlos con precisión. JavaScript utiliza una representación de coma flotante de 64 bits, con las limitaciones asociadas. Para más información, leer ¿Por qué mis programas no pueden hacer cálculos aritméticos correctamente?.

Esto implica que existen números para los cuales su representación presenta problemas para redondear correctamente. Sin embargo, los métodos utilizados en esta respuesta resuelven esos problemas.

Además, existe otro comportamiento peculiar que se está evitando: Math.round() redondea un 5 en la primera posición no significativa de un negativo hacia el 0.

Math.round( 1.5); // ==  2
Math.round(-1.5); // == -1   -problema!
Math.round(-1.6); // == -2
Math.round( 0.5); // ==  1
Math.round(-0.5); // == -0   -sí, "-0"

En la siguiente prueba, se corrigió el redondeo de números negativos para Math.round() y se comparan los casos en los que puede fallar cada una.


Resultados de cada respuesta:

/* -----------------------------------------------------------
 *            Funciones de cada respuesta
 * ----------------------------------------------------------- */
function roundNumber (number, max = 2) {
  //Respuesta de Guz
  let fractionalPart = number.toString().split('.')[1];
  
  if (!fractionalPart || fractionalPart.length <= 2) {
    return number;
  }
  
  return Number(number.toFixed(max));
}

function mathRound (num) {
  //Respuesta de Rubén
  return Math.round(num * 100) / 100 ;
}

function mathRound2 (num, decimales = 2) {
  //Respuesta de Rubén modificada por mí para el caso general y números negativos
  var exponente = Math.pow(10, decimales);
  return (num >= 0 || -1) * Math.round(Math.abs(num) * exponente) / exponente;
}

function redondearDecimales (numero, decimales = 2) {
    //Respuesta de Enrique B.
    numeroRegexp = new RegExp('\\d\\.(\\d){' + decimales + ',}');
    if (numeroRegexp.test(numero)) {
        return Number(numero.toFixed(decimales));
    } else {
        return Number(numero.toFixed(decimales)) === 0 ? 0 : numero;
    }
}

function redondear(x, decimales = 2)
{   //Respuesta de ArtEze
 var texto=x+""
 var poco=texto.search("e-")
 if(poco>=0)
 {
  var decimales_salida=texto.slice(poco+2)*1
  return decimales<decimales_salida?0:x
 }
 var mucho=texto.search("e+")
 if(mucho>=0)
 {
  return x
 }
 var punto=texto.search("\\.")
 var cortado=texto.slice(0,punto+decimales+2)
 var longitud=cortado.length
 var decimales_ingresado=longitud-punto-1
 for(var i=decimales_ingresado;i<=decimales;i++)
 {
  cortado+="0"
 }
 longitud=cortado.length
 var último=cortado.slice(longitud-1)
 var anteúltimo=cortado.slice(longitud-2,longitud-1)
 if(último*1>=5)
 {
  anteúltimo=(anteúltimo*1)+1
 }
 cortado=cortado.slice(0,longitud-2)+""+anteúltimo
 return cortado*1
}

function intlRound (numero, decimales = 2, usarComa = false) {
    //Esta respuesta
    var opciones = {
        maximumFractionDigits: decimales, 
        useGrouping: false
    };
    return new Intl.NumberFormat((usarComa ? "es" : "en"), opciones).format(numero);
}

function round(num, decimales = 2) {
    var signo = (num >= 0 ? 1 : -1);
    num = num * signo;
    if (decimales === 0) //con 0 decimales
        return signo * Math.round(num);
    // round(x * 10 ^ decimales)
    num = num.toString().split('e');
    num = Math.round(+(num[0] + 'e' + (num[1] ? (+num[1] + decimales) : decimales)));
    // x * 10 ^ (-decimales)
    num = num.toString().split('e');
    return signo * (num[0] + 'e' + (num[1] ? (+num[1] - decimales) : -decimales));
}




/* -----------------------------------------------------------
 *            PRUEBAS
 * ----------------------------------------------------------- */
 
let funciones = [roundNumber, mathRound, mathRound2, redondearDecimales, redondear, intlRound, round],
    pruebas = [
      ["Básica", 1.445, 1.45],
        ["Negativo", -1.445, -1.45],
        ["Nueves", 1.997, 2],
        ["Número grande", 1.1e+21, 1.1e+21],
        ["Número chico", 0.0000007, 0],
        ["Número chico != 0", [9e-8,7], 1e-7],
        ["Problema con coma flotante", 1.005, 1.01]
    ],
    resultado = [];

pruebas.forEach(function(prueba){
    let numero    = prueba[1],
        esperado  = prueba[2],
        resPrueba = [prueba[0]+" ("+numero+" ⟶ "+esperado+")"];
    funciones.forEach(function(funcion){
        let devuelto = (typeof numero == 'number' ? funcion(numero) : funcion(...numero));
        if (devuelto == esperado) {
            resPrueba.push("✔ "+funcion.name);
        } else {
            resPrueba.push("❌ "+funcion.name+" ("+devuelto+")");
        }
    });
    resultado.push(resPrueba.join("\n\t"));
});

document.getElementById("resultado")
    .innerText = resultado.join("\n");
<pre id="resultado"></pre>


Sin embargo, la eficacia tiene su contra en performance (ver benchmark).

+--------------------+-----------+
|      Función       |  Ops/seg  |
+--------------------+-----------+
| mathRound          | 2,387,155 |
| mathRound2         | 1,531,698 |
| roundNumber        |   114,499 |
| redondear2         |   180,980 |
| redondearDecimales |    83,631 |
| round              |    50,800 |
| intlRound          |     2,243 |
+--------------------+-----------+
                      *Chrome 55


En conclusión:

  • La solución con Math.round() es la más rápida, pero falla con valores negativos y tiene problemas de precisión con coma flotante.
  • Al modificarla como mathRound2() (en esta misma respuesta), se amplió al caso general y se resolvió el error con valores negativos. Sigue siendo más rápida que el resto, pero aún no resuelve el error de precisión.
  • La solución con Intl.NumberFormat() de esta respuesta devuelve el resultado correcto en todos los casos, pero es significativamente más lenta que las demás.
  • La solución de round() de redondeo decimal es la que, cumpliendo todas las pruebas, mejor se desempeña. Es significativamente más lenta que mathRound2(), pero aún así logra 50k operaciones por segundo, algo más que aceptable para muchos escenarios, por lo que es la recomendada para el caso general en el que se quiera una respuesta precisa y sin errores de redondeo.
Mariano
  • 23,777
  • 20
  • 70
  • 102
  • El vaso medio lleno y medio vacío, cada uno lo ve de diferente manera. 1/2 está igual de cerca del cero que del 1, así que no está tan mal redondear para el otro lado. –  Feb 13 '17 at 15:29
  • 2
    @ArtEze Son 2 métodos diferentes de redondeo: redondeo aritmético simétrico (-1.5 ≈ -2) versus redondeo aritmético asimétrico (-1.5 ≈ -1). Matemáticamente es preferible el primero, que cumple con las identidades de módulo. – Mariano Feb 13 '17 at 16:11
4

Para resolver esto, lo que pensé fue transformar el número en un string, y luego de haberlo procesado, volver a convertirlo en un número. El proceso se trata de partir el número justo cuando aparece el punto. También considero que puede venir la letra e, que marca potencias de 10. Si existe e-, es un número tan pequeño que se convierte en 0, pero existe e+, lo deja igual que el número ingresado.

Si no existe e+ ni e-, busca el punto, corta el texto en una posición que es la suma entre el lugar del punto y la cantidad de decimales. Además deja espacio extra, que será decisivo en aproximar el número. Ese último caracter, si es igual o mayor a 5, aumenta en 1, el anteúltimo decimal.

Aveces lo que se ingresa tiene pocos decimales, y queremos aproximar con más decimales que eso, pero no es problema, ya que podemos rellenar el número con ceros a la derecha, y seguir procesando normalmente.

Código:

function redondear(x,decimales)
{
 var texto=x+""
 var poco=texto.search("e-")
 if(poco>=0)
 {
  var decimales_salida=texto.slice(poco+2)*1
  return decimales<decimales_salida?0:x
 }
 var mucho=texto.search("e+")
 if(mucho>=0)
 {
  return x
 }
 var punto=texto.search("\\.")
 var cortado=texto.slice(0,punto+decimales+2)
 var longitud=cortado.length
 var decimales_ingresado=longitud-punto-1
 for(var i=decimales_ingresado;i<=decimales;i++)
 {
  cortado+="0"
 }
 longitud=cortado.length
 var último=cortado.slice(longitud-1)
 var anteúltimo=cortado.slice(longitud-2,longitud-1)
 if(último*1>=5)
 {
  anteúltimo=(anteúltimo*1)+1
 }
 cortado=cortado.slice(0,longitud-2)+""+anteúltimo
 return cortado*1
}
var lista=[1.445, -1.445, 1.1e+21, 0.0000007, 1.005]
for(var i=0;i<lista.length;i++)
{
 var actual=redondear(lista[i],2)
 console.log(lista[i],actual)
}
  • 1
    En el caso de números chicos, hay casos en los que no debería devolver `0`. Por ejemplo `redondear(5e-7,6)` debería devolver `0.000001` – Mariano Feb 13 '17 at 22:55
  • @Mariano Tomando en cuenta los dígitos significativos sí, pero los dos primeros dígitos del número son 00. –  Oct 24 '17 at 15:15
  • Creo que no entendí tu comentario de recién... Hasta donde recuerdo, la idea era redondear a N dígitos significativos. Lo que comenté es que este método, si bien funciona en la mayoría de los casos, falla con el ejemplo que te di, con números chicos que JavaScript los presenta en notación científica, y al tratarlos como string, no se está considerando bien el redondeo. No entiendo lo de "los dos primeros dígitos". En el caso que te comenté, el resultado esperado era `0.000001`. En [mi respuesta](https://es.stackoverflow.com/a/49177/127) puse más detalles de los casos. – Mariano Oct 24 '17 at 15:57
  • 2
    @Mariano Me confundí yo, no había visto el 6, tenías razón. –  Oct 24 '17 at 16:22
3

Respuesta corta

Usa Math.round(valor * 100) / 100

Explicación

Se incluye esta respuesta por ser un solución popular, sin embargo, esta introduce un error de redondeo el cual podría o no ser relevante. Para profundizar en esto véase Math.round()

Fragmento

var valor = [
  10,
  1.77777777,
  9.1
];
var resultado = valor.map(function(num){
  return Math.round(num * 100) / 100 ;
});
console.log(resultado);

Respuesta inspirada por respuesta a Round to at most 2 decimal places

Rubén
  • 10,857
  • 6
  • 35
  • 79
  • 2
    Mejor es [ésta respuesta](http://stackoverflow.com/a/12830454), tiene mayor precisión. JavaScript nunca fue preciso como otros lenguajes con el manejo complejo de números. – gugadev Feb 11 '17 at 13:43
  • @Guz: Pues anímate a publicar esa u utra "de tu ronco pecho" (frase mexicana) – Rubén Feb 11 '17 at 14:10
  • 1
    Para el caso general, en vez de `Math.round(num * 100) / 100`, sería `var exponente = Math.pow(10, decimales); return Math.round(num * exponente) / exponente` – Mariano Feb 13 '17 at 08:12
  • 2
    Otro tema a tener en cuenta es que no redondea correctamente los números negativos. Para corregirlo: `var exponente = Math.pow(10, decimales); return (num >= 0 || -1) * Math.round(Math.abs(num) * exponente) / exponente` – Mariano Feb 13 '17 at 08:45
  • Gracias @Mariano. Creo que valdría mucho la pena que publicaras tus comentarios como una respuesta. – Rubén Feb 13 '17 at 11:59
2

Existen muchas maneras de redondear un número así como varias formas de redondeo. El redondeo clásico (aumentar una unidad cuando la parte fraccionaria sea >= 5), se puede hacer mediante Math.round o si se desea un redondeo solo de la parte fraccionaria, se puede usar toFixed.

Ejemplo

/**
 * Redondea la parte fraccionaria de un número
 * solo cuando ésta es mayor a 2 dígitos.
 *
 * @param {number} number: número a redondear
 * @param {number} max: máximo de dígitos fraccionales
 */
function roundNumber (number, max = 2) {
  if (typeof number !== 'number' || isNaN(number)) {
    throw new TypeError('Número inválido: ' + number);  
  }
  
  if (typeof max !== 'number' || isNaN(max)) {
    throw new TypeError('Máximo de dígitos inválido: ' + max); 
  }
  
  let fractionalPart = number.toString().split('.')[1];
  
  if (!fractionalPart || fractionalPart.length <= 2) {
    return number;
  }
  
  return Number(number.toFixed(max));
}

/* Pruebas */
try {
  console.log(roundNumber(3.4589));
  console.log(roundNumber(0));
  console.log(roundNumber(2.42498, 3));
  console.log(roundNumber(NaN, 2));
  console.log(roundNumber(5.38024, null));
} catch (e) {
   console.error('Ups, ha ocurrido un error: ', e.message); 
}
gugadev
  • 18,776
  • 1
  • 24
  • 49
  • Ya lo edité para añadir pruebas. – gugadev Feb 11 '17 at 16:57
  • Claro, si te fijas al inicio de la función hay dos validaciones para cada parámetro. En las dos últimas pruebas, pasé `NaN` y `null` como parámetro para que te des cuenta que lanza un error con un mensaje personalizado que puedes capturar. – gugadev Feb 11 '17 at 17:00
  • No se muestra porque se ha lanzado un error en la línea anterior. Si comentas la penúltima prueba verás que se lanza. Poner un try catch se sobreentiende creo yo xD. De todos modos lo pondré. En caso no prefieras errores, puedes devolver cualquier cosa que tu veas pertinente. – gugadev Feb 11 '17 at 17:03
  • [Continuemos el debate en el chat](http://chat.stackexchange.com/rooms/53470/discussion-between-ruben-and-guz). – Rubén Feb 11 '17 at 17:05
  • Para ver la cantidad de decimales, en vez de convertir a string y ver el largo (muy poco eficiente), recomendaría usar: `if (number * Math.pow(10, max) % 1) { return Number(number.toFixed(max)); }; return number`... Como está actualmente, fallaría con `roundNumber(1.1e+21, 2)` – Mariano Feb 13 '17 at 08:02
0

Simple, sencilla, hermosa y precisa.

function redondear(valor, digitos){
    const base = 10**digitos;
    const precioxBase = valor*base;

    const rPrecioxBase = Math.round(precioxBase);

    const precioFinal = rPrecioxBase/base;
    return precioFinal;   
}
  • 3
    Por favor lee [answer], mas que el texto inicial (*el cual no aporta del todo*), se espera una explicación de lo que tu código hace y como este soluciona el problema – BetaM Sep 22 '20 at 16:53
-1

Favor fijate en este ejemplo sencillo y verifica si es lo que necesitas.

// b contiene un float
var b= 1.7777777;
// con toFixed delimitamos a 2 decimales pero 
//tambien hace el redondeo, es decir lo deja en 1,78
b= b.toFixed(2);
console.log(b); //lo hace bien pero es un string
console.log(typeof b); //comprobamos que es un string

b= parseFloat(b);//lo pasamos a float
console.log(b);//comprobamos
console.log(typeof b);//verificamos que es un float
  • 5
    Hola Guido. Bienvenido a [es.so]. Esta pregunta ya tiene marcada una respuesta como aceptada. además la tuya no aporta nada que no se haya contemplado en otras respuestasl. Te sugiero participar en preguntas sin respuestas aceptadas o cuyas respuestas no tengan lo que vas a aportar. Un saludo ^^ – lois6b Oct 24 '17 at 13:15
-1

Simplemente multiplicas por 100, haces un Math.round y divides entre 100

let numero = 5.55555555 
numeroRedondeado = Math.round(numero*100)/100
console.log(numeroRedondeado)//5.56
ÓscarMO
  • 1
  • 1
  • Esta solución introduce un error. Véase [esta respuesta](https://es.stackoverflow.com/a/48959/65) – Rubén May 17 '20 at 18:35