Formato coma flotante 64 bits: Double Precision IEEE754

Figura
Figura. Convertidor formato IEEE754

Representar los números decimales en coma flotante o notación científica consiste en convertir cualquier número en una expresión como m×10e, donde m es la mantisa y e el exponente. La mantisa es un valor real 1 ≤ m < 10, mientras que el exponente es un número entero cualquiera. Así el número 123.456 se representa en notación científica como 1.23456×102. Otro valor como 0.000789 se representaría como 7.89×10-4.

El formato IEEE754 usa algo parecido pero almacenando el número en formato binario. Por ejemplo, el número real 9.5 en binario es 1001.1. Con coma flotante lo representaríamos como 1.0011×23, usando potencias de dos pues es la base del sistema binario. El exponente vale tres pues ese es el número de lugares que hay que correr el punto decimal a la derecha.

El formato Double IEEE754 usa un registro de 64 bits con la siguiente estructura:

  • 1 bit para el signo.
  • 11 bits para el exponente.
  • 52 bits para la mantisa.

La mantisa de un binario en coma flotante podría ser como 1.b51b50...b1, es decir, un 1 y 51 dígitos fraccionarios. Como el dígito de la parte entera será siempre 1, podemos considerarlo como un bit implícito y no guardarlo con el registro. Así incrementamos un dígito más en la fracción b52b51...b1 hasta los 52 dígitos. Por lo tanto la estructura final subyacente es la siguiente, donde los 52 bits finales son para la fracción, que viene a ser la parte fraccional de la mantisa:

  • s: 1 bit para el signo.
  • e: 11 bits para el exponente.
  • b: 1 bit implícito.
  • f: 52 bits para la fracción.

El valor decimal responderá al cálculo de (-1)s × b.f × 2e con s, b, f en dígitos binarios y el exponente e convertido a decimal, tras aplicar un determinado offset y algunas otras consideraciones que ya explicaremos. En el siguiente ejemplo interactivo podrá probar números y ver como se representan en IEEE754:

Ejemplo: Valores

Números significativos
Decimal num.toString():
Decimal más cercano num.toExponential(20):
Hexadecimal num.toString(16):
Binario num.toString(2):
Signo (1): s = ?
Exponente (11): E = ? ⇒ e = E10-1023 = ?-1023 = ?
Bit implícito (1): b = ?
Fracción (52): f = ?
Representación: (-1)s × b.f × 2e =

Double IEEE754 Hexadecimal (16):

Partiendo del Double IEEE 754 anterior, recuperamos ahora su valor decimal para corroborar la correcta conversión:

Decimal recuperado:

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Valores significativos relacionados con el formato IEEE754 en JavaScript

El objeto Number dispone de algunas valores significativos que sirven a modo de constantes y cuyo formato IEEE754 podemos obtener en el anterior convertidor. También existen en el convertidor las constantes MAX_DENORMAL y MIN_NORMAL que no forman parte de las disponibles en Number. Uso esas dos constantes en el código que implementa el convertidor y creo conveniente contemplarlas pues suponen dos valores significativos para la representación del formato IEEE754. Todos estos valores significativos se irán explicando en este tema y los siguientes.

Con el siguiente ejemplo podrá recuperar esos valores significativos directamente del navegador que esté usando.

Ejemplo: Valores significativos de Number en JavaScript

PropiedadValor
Window.NaN
Window.Infinity
PropiedadValor
Number.NaN
Number.POSITIVE_INFINITY
Number.NEGATIVE_INFINITY
Number.MAX_VALUE
Number.MIN_VALUE
Number.MAX_SAFE_INTEGER
Number.MIN_SAFE_INTEGER
Number.EPSILON
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Convirtiendo números a Formato IEEE754

Vamos a explicar como pasar un número decimal a un hexadecimal que representa el formato doble IEEE754. Diferenciaremos varios casos siempre con valores no negativos, pues el negativo es exactamente igual sólo que el primer bit del formato se pone a 1:

  1. Número real normalizado:
  2. Número entero en el rango MIN_SAFE_INTEGER ≤ n ≤ MAX_SAFE_INTEGER, exceptuando 0, como n = 1234
  3. Número real denormalizado en el rango MIN_VALUE ≤ n ≤ MAX_DENORMAL, como n = 3.7×10-310
  4. Número 0
  5. Número Infinity
  6. Número NaN

La escala de números reales positivos que podemos representar con el formato de coma flotante se agrupa en los denormalizados y los normalizados. Según que el número a convertir esté en uno u otro rango tendremos que aplicar una técnica de conversión distinta. A continuación puede ver los rangos usando constantes, potencias de 2 y potencias de 10:

DenormalizadosNormalizados
DesdeHastaDesdeHasta
MIN_VALUEMAX_DENORMALMIN_NORMALMAX_VALUE
2-10742-1022×(1-2-52)2-102221023×(2-2-52)
5×10-3242.2250738585072010×10-3082.2250738585072014×10-3081.7976931348623157×10308

Se observa que entre MAX_DENORMAL y MIN_NORMAL hay un espacio de 5×10-324, número que es también el mínimo valor MIN_VALUE representable.

const MAX_DENORMAL = Math.pow(2, -1022) * (1-Math.pow(2, -52));
const MIN_NORMAL = Math.pow(2, -1022);
console.log(MIN_NORMAL - MAX_DENORMAL); // 5e-324
console.log(Number.MIN_VALUE); // 5e-324
console.log(MIN_NORMAL * Number.EPSILON); // 5e-324
    

MIN_VALUE también es el espaciado para un exponente igual o menor que -1022, como explicaremos en el apartado sobre Number.EPSILON. Su valor se podría obtener también así:

MIN_VALUE = MIN_NORMAL × ε = 2-1022 × 2-52 = 2-1074 = 5×10-324

El exponente en el formato IEEE754

Para componer el exponente hemos de saber que IEEE754 usa un Offset binario de 1023. Con 11 bits del exponente abarcamos 211 = 2048 valores que van desde 0 hasta 2047. Con ese offset podemos representar exponentes positivos y negativos en el rango [-1022, +1023]:

ExponenteExponente - 1023
0Ver notas
1-1022
2-1021
···
1021-2
1022-1
10230
1024+1
1025+2
···
2045+1022
2046+1023
2047Ver notas

Vemos que por encima del cero está el exponente más pequeño 00000000001, que pasando a decimal y quitando el offset es -1022. Por lo tanto el valor normalizado más pequeño que podemos representar es 1.00··52··00 × 2-1022, con 52 ceros en la fracción, constante que vale 2-1022 y que hemos denominado MIN_NORMAL.

Para ahorrarnos escribir bits en este documento cuando pongamos un binario como 1.00··52··00 estaremos abreviando y dando a entender que la parte fraccional tiene 52 bits. Si es sólo un entero o está en la parte entera como 101··16··100.10101 nos referiremos entonces a los dígitos de esa parte entera. Abreviar los bits no supone que hayan de ser todos iguales.

Con el exponente 11111111110 en binario, 2046 decimal, quitando offset será +1023. Con este exponente obtenemos el máximo valor representable con todos unos en la fracción, es decir 1.11··52··11 × 21023. Podemos hacer estas operaciones para calcular ese valor, recordando que para un binario con n unos su valor es 2n-1:

MAX_VALUE = 1.11··52··11 × 21023 = 11··53··11 × 2-52 × 21023 = (253-1) × 2-52 × 21023 = (2 - 2-52) × 21023 = 21023 × (2 - ε) = 1.7976931348623157 × 10308

Otra forma de obtener ese valor es usando lo que comentamos sobre espaciado en un tema posterior. El espaciado en ese tramo es de 21023ε. Por otro lado el número 21024 es el siguiente al máximo valor 1.11··52··11 × 21023 y no es representable en el formato. Pero podemos restarle un espaciado para calcular el máximo valor:

MAX_VALUE = 21024 - 21023 × ε = 21024 - 21023 × ε = 21023 × (2 - ε) = 1.7976931348623157 × 10308

Vea que no podemos comprobar en JavaScript el valor máximo con n < 21024, pues 21024 supera el valor máximo. Para representar 21024 necesitaríamos un bit más en el exponente, pasando de 11 a 12 bits. En realidad si que podemos representar un valor mayor que el máximo pero nos lo devolverá como Infinity. En este código vemos que la comparación es cierta pues cualquier valor siempre será menor que Infinity:

console.log(123 < Math.pow(2, 1024)); // true
console.log(123 < Infinity); // true
console.log(Math.pow(2, 1024)); // Infinity
console.log(Math.pow(2, 1023) * (2 - Math.pow(2, -52)));
    //1.7976931348623157e+308
    

Sigamos con los exponentes, donde el primero y último tienen un cometido especial. El exponente 0 se usa para representar el cero con la fracción siendo todos ceros. Vea que tendríamos dos ceros, +0 y -0 con el bit del signo valiendo cero y uno respectivamente.

Si el exponente es 0 y la fracción es distinta de cero representa un número almacenado como denormalizado. Esta denormalización permite representar números más pequeños que MIN_NORMAL hasta el mínimo valor MIN_VALUE. En ese caso el exponente que son todos ceros se convierte en 00000000001, que en decimal es -1022. El bit implícito será ahora 0. Así tenemos que los denormalizados van desde 0.00··52··01 × 2-1022 hasta 0.11··52··11 × 2-1022, es decir, desde

MIN_VALUE = 0.00··52··01 × 2-1022 = 1.0 × 2-52 × 2-1022 = 2-1074 = 5 × 10-324

Hasta

MAX_DENORMAL = 0.11··52··11 × 2-1022 = (1.00··52··00 - 0.00··52··01) × 2-1022 = 1.00··52··00 × 2-1022 - 0.00··52··01 × 2-1022 = 2-1022 - MIN_VALUE = 2-1022 - 2-52 × 2-1022 = 2-1022 × (1 - 2-52) = 2-1022 × (1 - ε) = 2.225073858507201 × 10-308

El exponente 2047 se usa para representar Infinity cuando la fracción es todo ceros, distinguiéndose entre +Infinity y -Infinity según el signo. Si la fracción es distinta de ceros (todos unos) tendremos NaN, ignorándose el signo, es decir, no hay un +NaN y un -NaN.

Formato IEEE754 normalizado de un un número real mayor o igual que uno

En este apartado explicaremos como obtener el formato normalizado IEEE754 del número 9.5 que es ≥ 1. Se aplicará el normalizado pues 9.5 > MIN_NORMAL, es decir, 9.5 > 2-1022. Los siguientes apartados a este los destinamos a otros casos, comprendiéndose entonces por qué cada caso lleva un tratamiento distinto. Al mismo tiempo explicaremos los pasos que se usan en el siguiente código de la función decToDoubleHex(num) que he usado en el ejemplo anterior. He quitando algunas cosas (comentarios con puntos consecutivos) para este apartado para simplificar la explicación. El código completo lo puede ver en el enlace al pie del ejemplo del convertidor IEEE754.

const MIN_NORMAL = Math.pow(2, -1022);
function decToDoubleHex(num){
    let obj = {sig: "", exp: "", imp: "", fra: ""};
    num = parseFloat(num);
    if (isNaN(num)){
        //NAN.....................
    } else if (num===0){
        //+0 y -0 .......................
    } else if (Math.abs(num)>Number.MAX_VALUE){
        //+Infinity y -Infinity ...........
    } else {
        let numAbs = Math.abs(num);
        let numBin= numAbs.toString(2);
        //signo
        let sig = (num<0)?1:0;
        obj.sig = sig;
        //exponente y fracción
        let exp, fra;
        if (numAbs<MIN_NORMAL){
            //Denormalizados.........
        } else {
            //Normalizados
            let pos = numBin.indexOf(".");
            let hayPunto = (pos>-1);
            //Enteros (sin punto decimal) ............
            pos = pos-1;
            fra = numBin.replace(/\./, "");
            if (hayPunto && numAbs<1){
                //Normalizado < 1 ..............
            } else {
                //Normalizados ≥ 1
                fra = fra.substr(1);
            }
            if (fra.length>52){
                fra = fra.substr(0, 52)
            } else {
                fra += "0".repeat(52-fra.length);
            }
            obj.fra = fra;
            exp = pos+1023;
            let expBin = exp.toString(2);
            obj.exp = "0".repeat(11-expBin.length) + expBin;
            obj.imp = 1;
        }
    }
    //Hexadecimal con 16 dígitos que almacena el IEEE754
    let str = obj.sig + obj.exp + obj.fra;
    let numDouble = "";
    for (let i=0; i<str.length; i=i+4){
        numDouble += Number("0b"+str.substr(i,4)).toString(16);
    }
    numDouble = numDouble.toUpperCase();
    return numDouble;
}
    

En el código vemos la constante const MIN_NORMAL = Math.pow(2, -1022) que indica el mínimo valor de un número para considerarlo con la técnica normalizada. Supongamos que vamos a convertir el decimal num = 9.5. Necesitaremos descubrir las distintas partes del registro para conformar el formato de coma flotante:

  • sig: Signo (1 bit)
  • exp: Exponente (11 bits)
  • imp: Bit implícito (1 bit)
  • fra: Fracción (52 bits)

Obtenemos el signo fácilmente con let sig = (num<0)?1:0, resultando ser un cero. También obtenemos el binario 1001.1 con let numBin = numAbs.toString(2) usando el valor absoluto del número. Recuerde que numero.toString(base) convierte un número a otra base. Luego vemos que es mayor que MIN_NORMAL y por tanto tendremos que usar la técnica normalizada. A partir de aquí trabajaremos con el binario numBin.

Ahora tenemos el binario 1001.1 que vamos a convertir en el valor en coma flotante 1.0011×23. Así el primer binario 1 será el bit implícito, el binario 0011 será la fracción y tendremos un exponente 3 (en decimal).

Para llevar esto a cabo vemos que el binario 1001.1 tiene un punto decimal. La posición del punto es 4, contando desde cero. Anotamos la posición 3 anterior al punto, pues ese será el número de lugares que tendremos que correr la coma, lo que viene a ser el exponente. Tras esto eliminamos el punto con fra = numBin.replace(/\./, "") y luego recortamos el primer bit con fra = fra.substr(1) quedándonos con 0011. Finalmente completamos con ceros a la derecha hasta los 52 bits resultando en 001100··52··00.

Para obtener el exponente recordemos que era 3. Sumamos el offset exp = pos+1023 y lo pasamos a binario con exp.toString(2) que resultará en 10000000010 (valor decimal 1026).

Pues ya tenemos todos los bits para que nuestro registro de coma flotante almacene el número 9.5:

  • sig: 0
  • exp: 10000000010
  • imp: 1
  • fra: 0011000000000000000000000000000000000000000000000000

Concatenamos todo menos el bit implícito con obj.sig + obj.exp + obj.fra. Ese sería el binario que JavaScript almacena. En el ejemplo lo pasamos al hexadecimal 4023000000000000 y lo presentamos así:

9.5 ⇒ (-1)s × b.f × 2e = (-1)0 × 1.0011000000000000000000000000000000000000000000000000 × 23

Formato IEEE754 normalizado de un número entero distinto de cero

Vamos a buscar el formato para el entero 1234. Se aplicará el normalizado pues 1234 ≥ MIN_NORMAL, es decir, 1234 ≥ 2-1022. El binario de 1234 es 10011010010. El tratamiento es igual que el caso de un número real mayor o igual que uno, sólo que ahora no nay un punto decimal. Si no lo hay lo imaginamos con ceros a la derecha: 10011010010.000··52··000, lo cual simulamos con if (!hayPunto) pos = numBin.length. Así tratamos este número como el caso anterior.

//Normalizados
let pos = numBin.indexOf(".");
let hayPunto = (pos>-1);
//Enteros (sin punto decimal)
if (!hayPunto) pos = numBin.length;
pos = pos-1;
fra = numBin.replace(/\./, "");
if (hayPunto && numAbs < 1){
    //Normalizado < 1.............
} else {
    //Normalizados ≥ 1
    fra = fra.substr(1);
}
if (fra.length>52){
    fra = fra.substr(0, 52)
} else {
    fra += "0".repeat(52-fra.length);
}
    

Ahora la posición del caracter punto obtenida con indexOf() es pos = 11, con lo que el exponente será pos-1 = 10 y el número se va a representar como 1.0011010010000··52··000 × 210. Sumamos el offset al exponente quedando exp = 10 + 1023 = 1033, que en binario es 10000001001. La fracción se obtiene quitando el primer bit que será implícito con lo que quedará 0011010010000··52··000. El signo es 0.

Con el signo 0, exponente 10000001001 y fracción 0011010010000··52··000 concatenamos el registro final, cuyo hexadecimal es 4093480000000000, presentándolo también así:

1234 ⇒ (-1)s × b.f × 2e = (-1)0 × 1.0011010010000000000000000000000000000000000000000000 × 210

Formato IEEE754 normalizado de un número real menor que uno

Vamos a convertir en formato IEEE754 normalizado el número 0.007 que es < 1. Se aplicará el normalizado pues 0.007 ≥ MIN_NORMAL, es decir, 0.007 ≥ 2-1022. Ese número 0.007 tiene por binario 0.000000011100101011000000100000110001001001101110100101111001. La parte del código de la función decToDoubleHex() que diferencia este caso es la siguiente, donde recordamos que numBin es el resultado de (0.007).toString(2) para obtener su binario.

//Normalizados
let pos = numBin.indexOf(".");
let hayPunto = (pos>-1);
//Enteros (sin punto decimal) ...............
pos = pos-1;
fra = numBin.replace(/\./, "");
if (hayPunto && numAbs<1){
    //Normalizado < 1
    let pos1 = fra.indexOf("1");
    fra = fra.substr(pos1+1) + "0".repeat(pos1+1);
    pos = pos-pos1;    
} else {
    //Normalizados ≥ 1..............
}
if (fra.length>52){
    fra = fra.substr(0, 52)
} else {
    fra += "0".repeat(52-fra.length);
}
    

Obtenemos la posición 1 del caracter punto decimal, de tal forma que pos = pos-1 = 0, siendo en principio ese el número de lugares decimales que tendría que correr la coma. Pero como el valor absoluto del número es menor que 1, realmente tenemos que correr la coma hasta después de alcanzar el primer uno acompañándolo con un exponente negativo. Esto se busca con let pos1 = fra.indexOf("1"), resultando pos1 = 8, por lo que el exponente sería pos-pos1 = 0 - 8 = -8.

Así que nuestro número 0.007 en coma flotante va a quedar como 1.1100101011··52··1001 × 2-8. Obtenemos la fracción con los dígitos a partir del punto decimal quedando fra = 1100101011··52··1001. Para obtener el exponente del formato le sumamos el offset, teniendo finalmente exp = -8 + 1023 = 1015, cuyo binario es 01111110111.

Con el signo 0, el exponente 01111110111 y la fracción 1100101011··52··1001 construimos nuestro registro cuyo paso a hexadecimal resulta en 3F7CAC083126E979, presentándolo también así:

0.007 ⇒ (-1)s × b.f × 2e = (-1)0 × 1.1100101011000000100000110001001001101110100101111001 × 2-8

Formato IEEE754 denormalizado

Vamos a construir el formato IEEE754 para el número real n = 3.7×10-310 que está en el rango MIN_VALUE ≤ n ≤ MAX_DENORMAL. La parte del código de la función decToDoubleHex() que se encarga de los valores denormalizados es la siguiente:

let numAbs = Math.abs(num);
let numBin = numAbs.toString(2);
//signo
let sig = (num < 0)?1:0;
obj.sig = sig;
//exponente y fracción
let exp, fra;
if (numAbs < MIN_NORMAL){
    //Denormalizados
    let pos = numBin.indexOf("1");
    fra = numBin.substr(pos);
    if (fra.length > 52){
        fra = fra.substr(0, 52)
    } else {
        fra = "0".repeat(52-fra.length) + fra;
    }
    obj.fra = fra;
    exp = 1;
    obj.exp = "0".repeat(11);
    obj.imp = 0;
} else {
    //Normalizados
    ...
}
let str = obj.sig + obj.exp + obj.fra;
    

Como el número es menor o igual que MAX_DENORMAL que vale 2-1022 × (1 - ε) = 2.225073858507201 × 10-308 aplicaremos el denormalizado. Consiste en un exponente mínimo de -1022, que sumando offset 1023 será un exponente con valor decimal uno. Sin embargo en el registro se almacena con once bits puestos a cero 00000000000.

El binario obtenido con (3.7e-310).toString(2) resulta en un número muy largo: 0.000···00010001000001110001101010010101001110110100111001. Hay 1027 ceros después del punto de fracción y antes del primer uno. En el código localizamos donde está ese primer uno para tomar sólo los dígitos a la derecha, consiguiendo conformar una fracción de 52 bits significativos. El exponente en decimal (exp) valdrá uno y el binario (obj.exp) serán todos ceros, al igual que el bit implícito que también es cero.

Con el signo 0, el exponente 00000000000 y la fracción 000001·52··1001 componemos el registro cuyo valor hexadecimal es 0000441C6A54ED39, presentándolo también así:

3.7×10-310 ⇒ (-1)s × b.f × 2e = (-1)0 × 0.00000100010000011100011010100101010011101101001110011 × 2-1022

Formato IEEE754 para el número cero

El número cero es uno de los casos especiales del formato IEEE754. Esta es la parte del código de decToDoubleHex() que se encarga de eso:

let esMenosCero = (num===0 && 1/num < 0);
num = parseFloat(num);
if (isNaN(num)){
    //NAN
    ...
} else if (num===0){
    //+0 y -0
    obj.sig = (esMenosCero)?"1":"0";
    obj.exp = "0".repeat(11);
    obj.imp = "?";
    obj.fra = "0".repeat(52);
    obj.renum = obj.dec;
}...
    

La variables esMenosCero trata de determinar si el número num del que vamos a obtener el formato es un +0 o un -0, con objeto de aplicarle el correspondiente bit de signo. Más abajo explicaremos el motivo de eso. Vemos que exponente y fracción serán todos ceros. Mientras que el bit implícito se ignora. Pero el signo puede ser cero o uno como hemos dicho. Por lo tanto puede haber un cero positivo y otro negativo. El positivo tiene todo el registro de 64 bits puestos a cero mientras que el cero negativo es con el primer bit a uno y el resto ceros, por lo que su hexadecimal es 8000000000000000. El cero positivo se presenta así:

+0 ⇒ (-1)s × b.f × 2e = (-1)0 × 0.0000000000000000000000000000000000000000000000000000 × 2-1022

Y el cero negativo, que sólo cambia el exponente del signo:

-0 ⇒ (-1)s × b.f × 2e = (-1)1 × 0.0000000000000000000000000000000000000000000000000000 × 2-1022

Con este apartado ya podemos resolver una de las dudas planteadas:

//¿Por qué hay un cero negativo en JavaScript? ¿Para qué se usa?
console.log(typeof -0); // number
console.log(0 === -0); // true
console.log(-0 > 0); // false
console.log(-0 < 0); // false
    

El número más pequeño en valor absoluto que podemos representar en el formato IEEE754 es 5×10-324. Cualquier número más pequeño que ése como 5×10-325 resultará 0. Será +0 si el número es positivo como para +5×10-325, aunque el signo puede obviarse. Y será -0 si el número es negativo como para -5×10-325. De alguna forma el signo del cero nos da información desde dónde se aproxima a cero el número:

console.log(5e-324);  // 5e-324
console.log(5e-325);  // 0
console.log(-5e-325); // -0
    

Como cualquier valor más pequeño que 5×10-324 es cero, ya podemos resolver una de las dudas planteadas:

//¿Por qué estas dos expresiones no son iguales para ese valor de k?
let k = 5e-324;
console.log(10*k === 100*(k/10)); // false
//...y sin embargo si lo es para este valor
k = 5e-323;
console.log(10*k === 100*(k/10)); // true
    

Si obtenemos los resultados parciales vemos que k/10 < 5×10-324 y por tanto lo aproxima a cero. Así que dado que JavaScript resuelve las expresiones desde los paréntesis interiores hacia fuera, primero resuelve ese cero y luego lo multiplica por 100 resultando cero, lo que es distinto de la parte izquierda 10×k:

let k = 5e-324;
console.log(10*k); // 5e-323
console.log(k/10); // 0
    

Con la segunda parte siendo k = 5×10-323 no hay problema pues k/10 = 5×10-324, siendo aún un número representable y no aproximándolo a cero.

El signo de los ceros entra en juego cuando realizamos operaciones aritméticas. Por ejemplo con la multiplicación:

console.log(0 * 2);   //  0
console.log(0 * -2);  // -0
console.log(-0 * 2);  // -0
console.log(-0 * -2); //  0
    

Ademas un valor finito dividido por +0 resulta Infinity y por -0 dará -Infinity:

console.log(1/0);   // Infinity
console.log(1/-0); // -Infinity
    

Esto nos servirá para resolver otra duda que habíamos planteado con los ceros y que era la siguiente:

//¿Cómo podemos saber si un valor es un cero negativo?
let n = -0;
//Comparando con -0 y +0 no funciona, ambos nos da verdadero
console.log(n === -0); // true
console.log(n === 0);  // true
//Si usamos esta expresión si funcionará ¿Por qué?
n = -0; console.log(n===0 && 1/n<0); // true
n = 0; console.log(n===0 && 1/n<0); // false 
    

Podríamos necesitar saber si un número en JavaScript es un cero positivo o negativo. Como es el caso del código para obtener el formato IEEE754, dónde necesitamos saberlo para determinar el bit de signo. Vea que no podemos comparar el número con +0 o -0, por que +0 === -0 es siempre cierto.

Pues la forma de saberlo es obteniendo el resultado de 1/n dándonos +Infinity o -Infinity si el número es +0 o -0 respectivamente. Y aunque siempre es cierto que +0 === -0 en cambio resulta que +Infinity !== -Infinity. Así que podemos realizar la comparación n===0 && 1/n<0 para detectar un cero negativo y n===0 && 1/n>0 para detectar un cero positivo.

Formato IEEE754 para números infinitos

En el formato IEEE754 podemos representar +Infinity y -Infinity. La parte del código de la función decToDoubleHex() que maneja estos dos números especiales es:

...
} else if (!Number.isFinite(num)){
    //+Infinity y -Infinity
    obj.sig = (num<0)?1:0;
    obj.exp = "1".repeat(11);
    obj.imp = "?";
    obj.fra = "0".repeat(52);
    obj.renum = obj.dec;
} ...   
    

El máximo valor absoluto de un número es MAX_VALUE = 21023 * (2 - ε) = 1.7976931348623157 × 10308. Así si n > MAX_VALUE entonces n = +Infinity, mientras que si n < -MAX_VALUE tendremos que n = -Infinity. Pero sobre esto hay que indicar lo que exponemos en el apartado dónde empieza Infinity de un tema posterior, aclarándose por qué debemos usar el método Number.isFinite(num) para saber si un número es o no infinito.

El registro del formato es un bit cero o uno para el signo, todos los bits del exponente puestos a uno y todos los bits de la fracción puestos a cero. Esto resultará en los headecimales 7FF0000000000000 para +Infinity con esta presentación:

+ ⇒ (-1)s × b.f × 2e = (-1)0 × 0.0000000000000000000000000000000000000000000000000000 × 21024

Y el hexadecimal FFF0000000000000 para -Infinity con la siguiente presentación:

- ⇒ (-1)s × b.f × 2e = (-1)1 × 0.0000000000000000000000000000000000000000000000000000 × 21024

Comentaremos más cosas en el apartado números infinitos de otro tema.

Formato IEEE754 para NaN

El valor NaN significa Not a Number y se representa en el formato IEEE754 como un caso especial. La parte del código del ejemplo es el siguiente:

...
if (isNaN(num)){
    //NAN
    obj.sig = "0";
    obj.exp = "1".repeat(11);
    obj.imp = "?";
    obj.fra = "1".repeat(52);
    obj.renum = obj.dec;
}...
    

Se conforma con un signo cero, exponente y fracción con todos los bits puestos a uno. Esto nos dará un valor hexadecimal de 7FFFFFFFFFFFFFFF y esta presentación:

NaN ⇒ (-1)s × b.f × 2e = (-1)0 × 0.1111111111111111111111111111111111111111111111111111 × 21024

En JavaScript sólo hay una representación NaN. Pues en el formato IEEE754 sólo es necesario un exponente con todos unos, cualquier signo y una fracción distinta de cero, lo que da lugar a que el número de representaciones de NaN sea muy superior, hasta 253-2 representaciones.

Hay más información en el apartado número NaN de otro tema.