Wextensible

El problema del redondeo al entero más cercano

Figura
Figura. El formato IEEE754 redondea el número cuando hay un exceso de bits.

Entender el redondeo que aplica el formato IEEE754 es imprescindible para saber por qué suceden algunas cosas con los números en JavaScript. Por ejemplo, por qué la operación 1.33 × 1.4 = 1.862 nos da el resultado 1.8619999999999999 en JavaScript, por debajo del valor esperado. Mientras que 1.33 × 1.1 = 1.463 resulta 1.4630000000000003, por encima del valor.

Conoceremos la respuesta a esas dudas en este tema. Pero antes conviene conocer qué significa redondear un número fraccionario y las distintas reglas de redondeo que podemos aplicar. Para redondear a entero un número fraccionario con respecto a una base entera se divide el fraccionario entre esa base y se redondea al entero más cercano, entero que a continuación se multiplica por la base para obtener el número redondeado:

entero = Round(numero / base) redondeado = entero × base

Por ejemplo, supongamos que tenemos 207 huevos y que se presentan en envases de una docena. Si he de redondear a envases calcularía la parte entera de 207 / 12 = 17.25 que sería 17, multiplicando luego 17 × 12 = 204 para obtener el valor redondeado.

Por lo tanto 207 huevos se redondea a 204 huevos para que ocupen exactamente 17 envases enteros de docena. El método usado para el redondeo es buscar el entero más cercano. Es obvio que entre los dos enteros consecutivos 17 y 18, el primero está más cerca de 17.25 que el segundo.

En el redondeo de decimales la base es la fracción hasta donde queremos llegar. Por ejemplo, podemos redondear hasta el segundo dígito fraccionario, siendo entonces la base 0.01 = 10-2. En general para unos determinados dígitos tenemos la base 10-digitos. Podemos modificar los cálculos anteriores y expresar la base con exponente positivo 10digitos, de tal forma que las operaciones ahora serían primero multiplicar y luego dividir:

base = 10digitos entero = Round(numero × base) redondeado = entero / base

Así por ejemplo el número 1.12 se redondea a 1 dígito resultando 1.1, siendo una aproximación más cercana con un dígito fraccionario:

base = 101 = 10 entero = Round(numero × base) = Round(1.12 × 10) = Round(11.2) = 11 redondeado = entero / base = 11 / 10 = 1.1

Si el número hubiese sido 1.16 el resultado obviamente sería 1.2 pues es un aproximación más cercana al valor.

El problema que nos va a ocupar este tema se centra en la ejecución de la función Round(numero) que redondea un número real a un entero. Con el ejemplo redondeamos 11.2 hacia abajo a 11. Y 11.6 hacia arriba a 12. La clave está en considerar la distancia entre el número real y el entero redondeado, entero que deberá ser la mejor aproximación posible al número real. O dicho de otra forma, que la distancia entre el entero y el real sea la menor posible.

Reglas de redondeo al entero más cercano

¿Pero qué pasa si la distancia en ambas direcciones es la misma? Considerando que el entero es 11.5 ¿hacia dónde redondeamos? Y aquí es donde entran en juego los convenios de redondeo. Se trata de unas reglas de desempate para ese caso.

La siguiente tabla muestra las reglas de redondeo al entero más cercano cuando está a igual distancia de dos enteros consecutivos, es decir, cuando la fracción sea exactamente 0.5:

Entero más cercanoEjemplo +Ejemplo -
Hacia +∞Round(11.5) = 12Round(-11.5) = -11
Hacia -∞Round(11.5) = 11Round(-11.5) = -12
Hacia ceroRound(11.5) = 11Round(-11.5) = -11
Lejos de ceroRound(11.5) = 12Round(-11.5) = -12
Que sea parRound(11.5) = 12Round(-11.5) = -12
Round(12.5) = 12Round(-12.5) = -12
Que sea imparRound(11.5) = 11Round(-11.5) = -11
Round(10.5) = 11Round(-10.5) = -11

Las reglas tratan de arreglar problemas de desviaciones de redondeo cuando se acumulan resultados que a su vez fueron objeto de redondeo. Por un lado tenemos las desviaciones debido a que haya una mayor probabilidad de que un resultado parcial tenga una fracción exacta de 0.5. Por ejemplo, supongamos que tenemos una lista muy larga de números reales positivos y muchos números tiene fracción exacta 0.5. Si usamos cualquiera de las cuatro primeras de la tabla anterior se daría una desviación importante que desvirtuaría el resultado. Esta desviación sin embargo la soluciona las dos últimas reglas par/impar.

Otra desviación es debida al desequilibrio del valor absoluto entre positivos y negativos que vemos en las dos primeras reglas de redondear a más y menos infinito. Con ellas redondear 11.5 resulta 12 mientras que -11.5 resulta -11. Si una lista de números son todos con fracciones 0.5 pero con valores positivos y negativos con igual probabilidad, entonces redondear arriba o abajo producirá desviaciones importantes. Las reglas a cero y par/impar son equilibradas y arreglan ese problema.

Las reglas par o impar resuelven ambos problemas. En los siguientes apartados se explica todo esto con más detalle. En el siguiente ejemplo interactivo puede probar con ambas desviaciones. Si usamos i iteraciones, la ejecución se realizará de la siguiente forma para cada opción del test:

Ejemplo: Desviaciones acumuladas

Ejemplo de desviación acumulada
Valor medio esperado en torno a
MétodoResultado% desviación sobre
valor sin redondeo
Sin redondeo
redondearMasInfinito()
redondearMenosInfinito()
redondearACero()
redondearLejosDeCero()
redondearAPar()
redondearAImpar()
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Cuántas más iteraciones hagamos más se evidencian los resultados, manifestándose con más claridad que las dos últimas reglas a par e impar producen las menores desviaciones. Con un millón de iteraciones, el máximo del ejemplo, las desviaciones no son superiores al 0.2% en valor absoluto en ambos test para las reglas par e impar.

Reglas para redondear hacia más infinito y menos infinito

Las dos primeras regla más infinito y menos infinito pueden dar lugar a desviaciones importantes con una serie de cálculos consecutivos de un mismo signo cuando la mayor parte de las veces nos devuelva fracciones 0.5.

Hagamos un ejemplo usando el método Math.random() que nos devuelve un real no negativo del rango [0, 1). Usando la primera regla si sumamos 1000 veces una fracción aleatoria de ese rango al valor inicial 0, la media de veces de ejecución debería estar en torno a 500. Pero si forzamos que la mitad de las veces la fracción sea 0.5 tendremos un valor que tiende a 750. Habremos producido una desviación acumulada media de +250:

function redondearMasInfinito(numero){
    return Math.floor(numero+0.5)
}
console.log(redondearMasInfinito(11.5));  //  12
console.log(redondearMasInfinito(-11.5)); // -11
let x  = 0;
for (let i=0; i<1000; i++){
    let n = Math.random();
    //Forzamos mitad de sumas con 0.5
    if (i%2) n = 0.5;
    x = redondearMasInfinito(x+n);
}
console.log(x); // En torno a 750
//Sin forzado el valor medio estaría en torno a 500
    

La función de redondeo redondearMasInfinito() es el resultado de floor(n + 0.5), que también podemos escribir como ⌊ n + 0.5 ⌋. Se basa en las que aparecen en la página Wikipedia: Rounding. La función floor(n) se implementa en JavaScript con el método Math.floor(n), que devuelve el mayor entero menor o igual que el número dado.

Por ejemplo, floor(1.5) = 1 y floor(-1.5) = -2, observándose que redondea hacia menos infinito, produciendo un distinto comportamiento cuando el número es positivo o negativo. Si sumamos 0.5 ese redondeo pasa a más infinito, pues obtenemos floor(1.5 + 0.5) = 2 y floor(-1.5 + 0.5) = -1, manteniéndose el distinto comportamiento entre positivos y negativos.

// Math.floor() redondea a menos infinito
console.log(Math.floor(1.5));  // 1
console.log(Math.floor(-1.5)); //-2
// Sumando 0.5 redondea a más infinito
console.log(Math.floor(1.5 + 0.5)); // 2
console.log(Math.floor(-1.5 + 0.5)); // -1
    

Con la regla a menos infinito sucede lo contrario. La desviación producirá una resultado en torno a 250, en lugar del promedio aleatorio 500. La desviación acumulada introducida es de -250:

function redondearMenosInfinito(numero){
    return Math.ceil(numero-0.5);
}
console.log(redondearMenosInfinito(11.5));  //  11
console.log(redondearMenosInfinito(-11.5)); // -12
let x  = 0;
for (let i=0; i<1000; i++){
    let n = Math.random();
    //Forzamos mitad de sumas con 0.5
    if (i%2) n = 0.5;
    x = redondearMenosInfinito(x+n);
}
console.log(x); // En torno a 250
//Sin forzado el valor medio estaría en torno a 500
    

La función redondearMenosInfinito() es el resultado de ceil(n - 0.5) = ⌈ n - 05 ⌉, que se implementa en JavaScript con Math.ceil(n). Por ejemplo, ceil(1.5) = 2 y ceil(-1.5) = -1, de tal forma que la función devuelve el menor entero mayor o igual que el número dado. Vemos que redondea hacia más infinito, produciendo también un valor absoluto distinto según el signo. Restando la fracción 0.5 el redondeo es hacia menos infinito pues ceil(1.5 - 0.5) = 1 y ceil(-1.5 - 0.5) = -2.

// Math.ceil() redondea a más infinito
console.log(Math.ceil(1.5));  // 2
console.log(Math.ceil(-1.5)); //-1
// Restando 0.5 redondea a menos infinito
console.log(Math.ceil(1.5 - 0.5)); // 1
console.log(Math.ceil(-1.5 - 0.5)); // -2
    

Otro problema de las reglas anteriores es que no están equilibradas en cuanto a valores positivos y negativos. Si eso se da con igual probabilidad se introducirá también desviaciones significativas. En el ejemplo siguiente extraemos una fracción aleatoria [0, 1) con una probabilidad del 50% de que la fracción sea exactamente 0.5. La mitad de las veces sumamos esa fracción y la otra mitad la restamos. Sin redondeo vamos a obtener un resultado en torno a 500. Mientras que con redondeo se produce una desviación de ±250 para los redondeos anteriores respectivamente:

let x = 500, y = 500, z = 500; 
for (let i=0; i<1000; i++){
    let n = Math.random();
    if (Math.random()<=0.5) n = 0.5;
    //Forzamos mitad de sumas negativos
    if (i%2) n = -n;
    x += n;
    y += redondearMasInfinito(n);
    z += redondearMenosInfinito(n);
}
console.log({x, y, z}); // Object {x: 503.0221302420839, y: 761, z: 246}
//Obtenemos x en torno a 500, y en torno a 750, z en torno a 250
    

Es conveniente recordar que el único método que tenemos en JavaScript para redondear números es Math.round(n), que redondea un real al entero más cercano usando la regla hacia +∞. Observe en esta muestra como se obtienen los mismos resultados:

function redondearMasInfinito(numero){
    return Math.floor(numero+0.5)
}
for (let i=-5; i<5; i++){
    let n = i+0.5;
    let x = redondearMasInfinito(n);
    let y = Math.round(n);
    console.log(n, x, y); 
    // -4.5 -4 -4
    // -3.5 -3 -3
    // -2.5 -2 -2
    // -1.5 -1 -1
    // -0.5  0 -0
    //  0.5  1  1
    //  1.5  2  2
    //  2.5  3  3
    //  3.5  4  4
    //  4.5  5  5
}
    

Los valores positivos se redondean hacia arriba, a +∞, como 4.5 redondea a 5. Pero los negativos también redondean a +∞, como -4,5 redondea hacia arriba a -4. Es por tanto Math.round() un método de redondeo desequilibrado en positivos y negativos.

Hay que tener en cuenta que métodos equivalentes en otro lenguajes como PHP que tiene round(numero, dígitos, regla) no se comportan igual. Las reglas posibles son lejos de cero (PHP_ROUND_HALF_UP), hacia cero (PHP_ROUND_HALF_DOWN), a par (PHP_ROUND_HALF_ODD) y a impar (PHP_ROUND_HALF_EVEN). Pero no tiene reglas a +∞ o -∞.

Reglas para redondear hacia cero y lejos de cero

Las reglas a cero y lejos de cero arreglan la desviación positivos y negativos. Se observa en la tabla del segundo apartado que están equilibradas en positivos y negativos. La primera a cero se implementa con la función sgn(n) × ⌈ |n| - 0.5 ⌉, o expresado también como sgn(n) × ceil(abs(n) - 0.5). En JavaScript podemos obtenerla con Math.sign(n) * Math.ceil(Math.abs(n) - 0.5). Producirá redondearACero(11.5) = 11 y redondearACero(-11.5) = -11, el mismo valor absoluto en ambos extremos.

La regla lejos de cero obedece a la función sgn(n) × ⌊ |n| + 0.5 ⌋ = sgn(n) × floor(abs(n) + 0.5), que podemos implementar con Math.sign(n) * Math.floor(Math.abs(n) + 0.5). Producirá redondearLejosDeCero(11.5) = 12 y redondearLejosDeCero(-11.5) = -12, el mismo valor absoluto en ambos extremos.

function redondearACero(numero){
    return Math.sign(numero) * Math.ceil(Math.abs(numero) - 0.5);
}
console.log(redondearACero(11.5));  //  11
console.log(redondearACero(-11.5)); // -11
function redondearLejosDeCero(numero){
    return Math.sign(numero) * Math.floor(Math.abs(numero) + 0.5);
}
console.log(redondearLejosDeCero(11.5));  //  12
console.log(redondearLejosDeCero(-11.5)); // -12
let x = 500, y = 500, z = 500;
for (let i=0; i<1000; i++){
    let n = Math.random();
    if (Math.random()<=0.5) n = 0.5;
    //Forzamos mitad de sumas negativos
    if (i%2) n = -n;
    x += n;
    y += redondearACero(n);
    z += redondearLejosDeCero(n);
}
console.log({x, y, z}); // Object {x: 501.3747678361181, y: 504, z: 496}
// Obtenemos x, y, z en torno a 500  
    

En el código anterior observará que no produce desviación en los redondeos, los tres valores estarán cercanos a 500 que es el valor sin redondeo.

Sin embargo estas reglas siguen produciendo desviación en una acumulación de resultados de un mismo signo cuando haya una gran probabilidad de fracciones 0.5, tal como vimos en el primer caso para el redondeo más y menos infinito. El siguiente ejemplo es el mismo utilizado para esos redondeos, pero ahora utilizando redondearACero(). Se observa que forzando la mitad de las sumas con 0.5 obtenemos un valor en torno a 250. Mientras que sin este forzado las fracciones aleatorias llevarían a un resultado en torno a 500 que debería ser el esperado.

let x  = 0;
for (let i=0; i<1000; i++){
    let n = Math.random();
    //Forzamos mitad de sumas con 0.5
    if (i%2) n = 0.5;
    x = redondearACero(x+n);
}
console.log(x); // En torno a 250
//Si forzado estaría en torno a 500
    

Esta desviación se soluciona con las reglas par/impar que veremos en el siguiente apartado.

Reglas para redondear a par e impar

Para la regla par se trata de redondear al entero par más cercano. Así 11.5 redondea al par más cercano 12, mientras que -11.5 redondea a -12. Pero también 12.5 redondea a 12 y -12.5 a -12. Vemos que hay un equilibrio entre signos y al mismo tiempo en un lado del signo el 50% redondeará arriba y el otro 50% lo hará abajo, pues la mitad de los enteros son pares y la otra mitad impares.

De forma equivalente sucede para la regla impar. Con 11.5 redondea al impar más cercano 11, mientras que -11.5 redondea a -11. Pero también 10.5 redondea a 11 y -10.5 a -11.

function redondearAPar(numero){
    return Math.floor(numero+0.5)-1+Math.abs(Math.sign((numero-0.5)%2));
}
console.log(redondearAPar(11.5), redondearAPar(12.5));   // 12   12
console.log(redondearAPar(-11.5), redondearAPar(-12.5)); //-12  -12
function redondearAImpar(numero){
    return Math.ceil(numero-0.5)+1-Math.abs(Math.sign((numero-0.5)%2));
}
console.log(redondearAImpar(10.5), redondearAImpar(11.5));   // 11   11
console.log(redondearAImpar(-10.5), redondearAImpar(-11.5)); //-11  -11
    

Las funciones que se aplican son ⌊n+0.5⌋-1+|sign((n-0.5)%2)| para redondeo a par y ⌈n-0.5⌉+1-|sign((n-0.5)%2)|. Se implementan fácilmente en JavaScript como puede ver en el código anterior.

Ambas reglas solucionan las dos desviaciones comentadas en apartados anteriores. La del caso de valores positivos con una alta probabilidad de resultados parciales con fracciones 0.5, como vemos en el ejemplo donde los resultados están en torno a 500 que es lo esperado, tal como podemos comprobar quitando el forzado a esa fracción:

let x  = 0, y = 0, z = 0;
for (let i=0; i<1000; i++){
    let n = Math.random();
    //Forzamos mitad de sumas con 0.5
    if (i%2) n = 0.5;
    x = x+n;
    y = redondearAPar(y+n);
    z = redondearAImpar(z+n);
}
console.log({x, y, z}); // Object {x: 500.1692800402766, y: 496, z: 497}
// Los tres valores estarán cercanos a 500
    

Y también arregla la desviación por igual probabilidad de resultados positivos y negativos, como se observa que obtenemos valores cercanos a 500:

let x = 500, y = 500, z = 500;
for (let i=0; i<1000; i++){
    let n = Math.random();
    if (Math.random()<=0.5) n = 0.5;
    //Forzamos mitad de sumas negativos
    if (i%2) n = -n;
    x += n;
    y += redondearAPar(n);
    z += redondearAImpar(n);
}
console.log({x, y, z}); // Object {x: 498.073370107979, y: 507, z: 491}
// Los tres valores estarán cercanos a 500
    

Por supuesto que para redondeo a par si la probabilidad de que los resultados parciales sean muchas veces de la forma k.5 siendo k un valor impar, se producirá un redondeo hacia arriba produciendo desviación. O cuando k fuera muchas veces par se produciría un redondeo hacia abajo. De forma equivalente pasaría con el redondeo impar.

El redondeo en el formato IEEE754

La regla redondeo a par es la utilizada en el formato IEEE754 cuando ha de buscar la presentación más cercana y ambas se encuentran a igual distancia. El redondeo en el formato IEEE754 se produce siempre usando números binarios. Por ejemplo, para estos números con parte entera impar:

11.2510 = 1011.012 ≈ 10112 = 1110 11.5010 = 1011.102 ≈ 11002 = 1210 11.7510 = 1011.112 ≈ 11002 = 1210

Mientras que para los siguientes pares:

12.2510 = 1100.012 ≈ 11002 = 1210 12.5010 = 1100.102 ≈ 11002 = 1210 12.7510 = 1100.112 ≈ 11012 = 1310

Se pueden deducir unas reglas de redondeo muy simples. Sea E y F cadenas de dígitos binarios. Formamos un número binario concatenándolos con dígitos cero y uno, como E.01F por ejemplo. Con este esquema podemos resumir la regla par de redondeo de la siguiente forma:

  1. E.0F ≈ E con ∀F: Si el primer bit fraccionario es un cero, la parte entera se queda como está, produciendo un redondeo hacia abajo. Y es indiferente lo que contenga el resto de la fracción.
  2. E.1F ≈ E+1 con F≠0: Si el primer bit de la fracción es 1 y el resto no es cero redondeáremos hacia arriba. Equivale a sumar 1 bit a la parte entera. Para saber que no es cero buscaríamos en esa cadena un primer uno. Cuando lo encontremos no necesitamos seguir buscando pues ya sabemos que no es cero.
  3. E0.1F ≈ E0 con F=0: En este caso hemos de comprobar que el resto de la fracción son todos ceros. Redondea hacia abajo pues la parte entera es par (acaba en cero).
  4. E1.1F ≈ E1+1 con F=0: Con el resto de la fracción todos ceros, redondea hacia arriba pues la parte entera es impar (acaba en uno).

Lo interesante de las reglas anteriores es que podrían implementarse para redondear fácilmente un número binario sin más que observar unos pocos bits. Con esto ya estamos en condiciones de responder a una de las duda planteadas:

//¿Cómo se explican estos resultados?
// 1.33 * 1.4 = 1.862
console.log(1.33*1.4); // 1.8619999999999999
// 1.33 * 1.1 = 1.463
console.log(1.33*1.1); // 1.4630000000000003
// 10.7 - 1.12 = 9.58
console.log(10.7-1.12);// 9.579999999999998
// 10.9 - 1.12 = 9.78
console.log(10.9-1.12);// 9.780000000000001
    

En los siguientes apartados veremos cada uno de los casos que responden a las cuatro sub-reglas de redondeo par.

Regla redondeo abajo: E.0F ≈ E con ∀F

Veamos el primer caso, por qué 1.33 × 1.4 ≠ 1.862 resultando 1.8619999999999999. La representación IEEE754 de los dos operandos que obtenemos en el convertidor IEEE754 son las siguientes.

1.33 = 1.0101010001111010111000010100011110101110000101001000 × 20 1.40 = 1.0110011001100110011001100110011001100110011001100110 × 20

Las operaciones se realizan en binario, por lo que procederemos a multiplicar esos dos números en nuestra calculadora binaria, pudiendo prescindir del exponente pues es cero:

1.0101010001111010111000010100011110101110000101001000 × 1.0110011001100110011001100110011001100110011001100110 = 1.1101110010101100000010000011000100100110111010010111011101···

El resultado puede darnos nos da muchos más bits que los 52 necesarios del formato IEEE754 en la parte fraccionaria. Hemos resaltado y presentado sólo los seis primeros.

Tras la operación JavaScript ha de redondear con la regla par a la representación IEEE754 más cercana. Como el primer bit en rojo es cero el redondeo se produce hacia abajo, es decir, la parte izquierda en azul no cambiará, finalizando en los existentes ··0111. A continuación convertimos el formato de ese binario en un IEEE754 y obtendremos su valor decimal usando una forma indirecta. Se trata de expresar el número como entero ajustando con un exponente, calcular el entero con el convertidor binario-decimal y finalmente usar una calculadora con mayor precisión que la de JavaScript para realizar la multiplicación:

1.1101110010101100000010000011000100100110111010010111 × 20 = 11101110010101100000010000011000100100110111010010111 × 2-52 = 8385702506163863 × 2-52 = 1.8619999999999998774313780813827

El número obtenido con la calculadora hemos de redondearlo manualmente a 16 dígitos fraccionarios siendo por tanto el que nos daba JavaScript 1.8619999999999999. Se observa la corrección de ese resultado usando el método toExponential() para obtener más dígitos significativos:

let n = 1.33 * 1.4;
console.log(n.toExponential(20)); // 1.86199999999999987743e+0
    

Entonces la multiplicación binaria 1.33 × 1.4 nos ha producido un binario que, tras redondearlo, resulta 1.8619999999999999. Y que no es el mismo que el valor 1.862 del resultado de la multiplicación decimal. Vea la diferente representación entre ambos números separados un bit, dígito que se origina por la operación binaria y el redondeo abajo usando la regla par:

1.8619999999999999 = 1.1101110010101100000010000011000100100110111010010111 × 20 1.8620000000000000 = 1.1101110010101100000010000011000100100110111010011000 × 20

Regla redondeo arriba: E.1F ≈ E+1 con F≠0

Veamos el segundo caso, por qué 1.33 × 1.1 ≠ 1.463 resultando 1.4630000000000003. La representación IEEE754 de los dos operandos que obtenemos en el convertidor IEEE754 son las siguientes.

1.33 = 1.0101010001111010111000010100011110101110000101001000 × 20 1.10 = 1.0001100110011001100110011001100110011001100110011010 × 20

Las operaciones se realizan en binario, por lo que procederemos a multiplicar esos dos números, pudiendo prescindir del exponente pues es cero:

1.0101010001111010111000010100011110101110000101001000 × 1.0001100110011001100110011001100110011001100110011010 = 1.0111011010000111001010110000001000001100010010011100100010···

Se observa claramente que la parte en rojo empieza por 10 pero a la derecha encontramos un primer uno. Así que hay que redondear hacia arriba sumando uno a la parte azul y quedando el binario 1.0111····101. Obtenemos el IEEE754 y calculamos su valor decimal como hicimos en el caso anterior:

1.0111011010000111001010110000001000001100010010011101 × 20 = 10111011010000111001010110000001000001100010010011101 × 2-52 = 6588766254843037 × 2-52 = 1.4630000000000003002043058586423

El valor obtenido con redondeo decimal es el esperado 1.4630000000000003 y que comprobamos con toExponential():

let n = 1.33 * 1.1;
console.log(n.toExponential(20)); // 1.46300000000000030020e+0
    

Finalmente observamos otra vez la diferencia de un bit entre el número 1.462 y 1.4630000000000003, bit que es el resultado de la operación binaria y el redondeo arriba:

1.4620000000000000 = 1.0111011010000111001010110000001000001100010010011100 × 20 1.4630000000000003 = 1.0111011010000111001010110000001000001100010010011101 × 20

Regla redondeo par abajo: E0.1F ≈ E0 con F=0

Veamos ahora por qué 10.7 - 1.12 ≠ 9.58, obteniéndose 9.579999999999998. Las representaciones de esos números que podemos obtener con el convertidor IEEE754 son las siguientes.

10.7 = 1.0101011001100110011001100110011001100110011001100110 × 23 1.12 = 1.0001111010111000010100011110101110000101000111101100 × 20

Tras quitar los exponentes procedemos a la resta usando la calculadora binaria:

1010.1011001100110011001100110011001100110011001100110 - 1.0001111010111000010100011110101110000101000111101100 1001.10010100011110101110000101000111101011100001010001

Al hacer la resta vemos que excede exactamente un bit 1 a la derecha resaltado en amarillo de los 52 bits que necesitamos para el formato. Ahora para conformar el registro IEEE754 se procede al redondeo de ese resultado. Como es una fracción 0.5, en binario 0.1, redondeará al par más cercano, que será la parte entera tal como está pues finaliza en cero.

Calculamos el valor decimal de ese formato:

1.0011001010001111010111000010100011110101110000101000 × 23 = 10011001010001111010111000010100011110101110000101 × 2-49 × 23 = 674132569222021 × 2-46 = 9.5799999999999982946974341757596

Comprobamos el cálculo con toExponential():

let n = 10.7 - 1.12;
console.log(n.toExponential(20)); // 9.57999999999999829470e+0
    

Observe que la diferencia de los formatos entre los numero 9.58 y el obtenido de la operación está en el último bit, diferencia originada por la operación y el redondeo par abajo:

9.579999999999998 = 1.0011001010001111010111000010100011110101110000101000 × 23 9.580000000000000 = 1.0011001010001111010111000010100011110101110000101001 × 23

Regla redondeo par arriba: E1.1F ≈ E1+1 con F=0

Hagámos la resta 10.9 - 1.12 ≠ 9.78 resultando 9.780000000000001. Los operandos tienen la representación siguiente:

10.9 = 1.0101110011001100110011001100110011001100110011001101 × 23 1.12 = 1.0001111010111000010100011110101110000101000111101100 × 20

Ahora esta es la resta binaria 10.9 - 1.12:

1010.1110011001100110011001100110011001100110011001101 - 1.0001111010111000010100011110101110000101000111101100 1001.11000111101011100001010001111010111000010100011111

Nuevamente tenemos una fracción binaria 0.1 (0.5 en decimal) de lo que excede de los 52 bits del formato. El número resultante en ese formato será redondeado al par más cercano que está hacia arriba pues el último bit de la parte entera es un uno, quedando 1.0011····10000. Calculemos su valor decimal:

1.0011100011110101110000101000111101011100001010010000 × 23 = 1001110001111010111000010100011110101110000101001 × 2-48 × 23 = 344103159028777 × 2-45 = 9.7800000000000011368683772161603

Se observa la corrección de ese resultado usando el método toExponential() para obtener más dígitos significativos:

let n = 10.9-1.12;
console.log(n.toExponential(20)); // 9.78000000000000113687e+0
    

Veamos las representaciones de 9.78 y 9.780000000000001 observando el bit de diferencia originado por la operación binaria y el redondeo par arriba:

9.780000000000000 = 1.0011100011110101110000101000111101011100001010001111 × 23 9.780000000000001 = 1.0011100011110101110000101000111101011100001010010000 × 23