Round to two decimals when necessary

5

I would like to round to two decimals, only when necessary . Below are examples of inputs and outputs

Entry:

10
1.7777777
9.1

Exit:

10
1.78
9.1

How can I do this in JavaScript?

Fragment

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

Inspired by Round to at most 2 decimal places

    
asked by Rubén 11.02.2017 в 14:26
source

6 answers

4

These two functions fulfill all the tests carried out.

1. Adapted from MDN ( recommended solution )

Taking the code from Rounding Decimal , I adapted the solution to perform a symmetric arithmetic rounding with negative numbers. That is, round -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. Using Intl.NumberFormat ()

The Intl.NumberFormat([locales[, options]]) function allows you to round a number correctly with native code. However, as will be seen later, this function accepts language-sensitive options, making it significantly slower.

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);
}

Returns a string with the representation of the number, rounded to the maximum of decimal places. If you want to convert the number, apply parseFloat() to the result.


Discussion

When manipulating numbers by performing floating point operations , there is a limitation to represent them accurately. JavaScript uses a 64-bit floating-point representation, with associated limitations. For more information, read Why can not my programs do arithmetic calculations correctly? .

This implies that there are numbers for which your representation has problems to round correctly . However, the methods used in this response solve those problems.

In addition, there is another peculiar behavior that is being avoided: Math.round ( ) rounds a 5 in the first non-significant position of a negative to 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"

In the next test, the rounding of negative numbers for Math.round() was corrected and the cases in which each one can fail are compared.


Results of each response:

/* -----------------------------------------------------------
 *            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>


However, effectiveness has its counterpart in performance (see 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


In conclusion:

  • The solution with Math.round() is the fastest, but it fails with negative values and has precision problems with floating point.
  • By modifying it as mathRound2() (in this same answer), it was extended to the general case and the error was resolved with negative values. It's still faster than the rest, but it still does not solve the precision error .
  • The solution with Intl.NumberFormat() of this response returns the correct result in all cases, but is significantly slower than the others.
  • The solution of% co_of% of rounding is the one that, fulfilling all the tests, performs better . It is significantly slower than round() , but still achieves 50k operations per second, something more than acceptable for many scenarios, so it is recommended for the general case in which you want an accurate answer and no rounding errors.
answered by 13.02.2017 / 13:27
source
4

You can also use the toFixed () method, even if you convert the number to a string and you would have to use the function Number () :

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

The bad thing is that it leaves 0 if the number does not contain decimal or is less than the parameter passed to toFixed (). This problem is eliminated with this small function:

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
    
answered by 11.02.2017 в 15:23
4

To solve this, what I thought was to transform the number into a string, and after having processed it, turn it into a number again. The process is about splitting the number just when the point appears. I also think that the letter e can come, which marks powers of 10. If there is e- , it is a number so small that it becomes 0, but there is e+ , leaving it equal to the number entered.

If there is no e+ or e- , find the point, cut the text into a position that is the sum between the place of the point and the number of decimals. It also leaves extra space, which will be decisive in approaching the number. That last character, if it is equal to or greater than 5, increases by 1, the second to last decimal.

Sometimes what is entered has few decimals, and we want to approximate with more decimals than that, but it is not a problem, since we can fill in the number with zeros to the right, and continue processing normally.

Code:

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)
}
    
answered by 13.02.2017 в 19:08
2

Short answer

Use Math.round(valor * 100) / 100

Explanation

This response is included because it is a popular solution, however, it introduces a rounding error which may or may not be relevant. For more information, see Math.round ()

Fragment

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

Response inspired by reply to Round to at most 2 decimal places

    
answered by 11.02.2017 в 14:26
1

There are many ways to round a number as well as several forms of rounding. Classic rounding (increase a unit when the fractional part is> = 5), can be done using Math.round or if you want a rounding only of the fractional part, you can use toFixed .

Example

/**
 * 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); 
}
    
answered by 11.02.2017 в 17:46
-1

Please look at this simple example and check if it is what you need.

// 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
    
answered by 24.10.2017 в 15:12