31

Pregunta Original: Why are two different numbers equal in JavaScript?

Estuve jugando un poco con la consola y se me ocurrió probar lo siguiente:

var num1 = 0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
var num2 =  0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
console.log(num1 == num2)

Y me sorprendió que realmente esta comparación se evalúa como verdadera (pueden probarla en consola).

¿Por qué pasa esto? Está claro que son números diferentes (el segundo tiene un dígito menos que el primero; si llego a agregar una F al segundo o un 0 al primero la comparación ya es falsa, pero mientras tanto, es verdadera).

JackNavaRow
  • 6,836
  • 5
  • 22
  • 49
Luis Masuelli
  • 484
  • 4
  • 12
  • 8
    No olvides poner las referencias usando links a las preguntas/respuestas originales – César Dec 06 '15 at 16:00
  • Como se les ocurre dar un downvote a esta pregunta? Tras que me rompo el lomo traduciendo una pregunta bastante gorda en SO original. – Luis Masuelli Dec 07 '15 at 14:41
  • Luis, te entiendo, muchos no estan de acuerdo con esto. Yo te di +1, por supuesto. – César Dec 07 '15 at 14:44
  • 1
    Si. No me referia a vos porque lo hablamos antes de hacer esto, incluso. Pero el que puso el DV es un mal companero y si esta en contra del sitio en espanol sencillamente no deberia participar. Algunos queremos traducir para popular este sitio web y que tenga participacion hispana para los que no son buenos en ingles y sin embargo son programadores (es raro pero los hay, y no hay por que segregarlos, especialmente si aceptamos que en portugues si existan) – Luis Masuelli Dec 07 '15 at 14:48
  • 1
    @LuisMasuelli yo de hecho le dí +1 a esta pregunta sin embargo no se deben olvidar que cada usuario es libre de votar como le parezca, un voto negativo significa "no estoy de acuerdo" – Carlos Muñoz Dec 07 '15 at 16:04
  • Todas las respuestas que publicaron son un copy paste de las respuestas que están es SO en ingles ! xd – vicasas Sep 06 '19 at 13:09
  • @vcasas, era la intención original del OP, su pregunta y respuesta en su momento fueron para llamar la atención y llenar con contenido popular del sitio en Inglés. – Mauricio Contreras Sep 06 '19 at 13:21
  • ¿Solucionó tu problema, @LuisMasuelli? Por favor no olvides marcar la respuesta como aceptada si tu problema se solucionó. Puedes hacerlo marcando el ✓ en la parte izquierda de la respuesta (se pondrá verde, ganarás 2 puntos de reputación y podrías acceder a [nuevos privilegios](https://es.stackoverflow.com/help/privileges)). ¡Mira [¿Qué debo hacer cuando alguien contesta mi pregunta?](https://es.stackoverflow.com/help/someone-answers) si tienes alguna duda! – JackNavaRow Sep 06 '19 at 13:21
  • @vcasas las primeras preguntas del sitio fueron las mas canonicas del sitio en ingles(copy/paste*), muchos usuarios se tomaron la molestia de traducirla para que el habla hispana pueda comprender mejor esta joyas de pregunta/respuesta – JackNavaRow Sep 06 '19 at 13:30

3 Answers3

37

Todos los números en JavaScript se representan internamente como números de punto flotante de precisión doble (ver §4.3.19 en la especificación), Esto quiere decir que uno puede representar exactamente cualquier número entre 0 y 9007199254740992 (hexadecimal 0x20000000000000, o lo que es lo mismo 2^53). Lo mismo ocurre para los números negativos: van de 0 a -9007199254740992. Si uno prueba agregar 1 (o restar 1 para los negativos) se da cuenta de que el número que queda es el mismo, pero si uno intenta agregar 2 se da cuenta de que el número cambia. Esto es por cómo es la mantisa en el estándar IEEE754.

Observe:

console.log(9007199254740992 === 9007199254740993)

Agregando otra F al segundo número (cuando lo representaste en hexa, estás cambiando no solo la mantisa sino también el exponente (el número es 4 órdenes mayor al otro), y por esto sí evalúa distinto.

a partir de la vesion de ECMAScript 2015 se incluyo una constante para obtener el número mas grande seguro* Number.MAX_SAFE_INTEGER, ademas de Number.isSafeInteger()

JackNavaRow
  • 6,836
  • 5
  • 22
  • 49
Luis Masuelli
  • 484
  • 4
  • 12
24

No se necesitan tantos '0's y 'F's. En realidad la igualdad comienza con solo 14 '0's y 'F's incluso con el operador ===

0x100000000000000 === 0xFFFFFFFFFFFFFF

La explicación es simple, en realidad esos dos números son iguales y por lo tanto la comparación es correcta de la misma manera que la siguiente igualdad también produce true

1 === 1.0

Simplemente son dos formas diferentes de expresar el mismo número.

Ambas representaciones hexadecimales evalúan al mismo número lo cual puede ser demostrado simplemente ejecutando en consola.

0x100000000000000
0xFFFFFFFFFFFFFF

Lo cual evalúa ambos a:

72057594037927940
72057594037927940

En el caso de la versión con muchos '0's o 'F's ambas expresiones evalúan al sgte número:

1.3407807929942597e+154
Carlos Muñoz
  • 12,864
  • 2
  • 42
  • 62
  • 7
    Esta respuesta sería perfecta si diese el motivo *real* de que esos números, que **no** son iguales, se evalúen como iguales: Todo número en JS se representa mediante IEEE754 y por tanto no hay precisión suficiente para distinguirlos. – Darkhogg Dec 07 '15 at 13:45
  • No es cierto que el motivo por el que `1 === 1.0` sea el mismo por el que esa comparacion es igual. El mecanismo subyacente es otro para la comparacion en la pregunta. Para la comparacion en la respuesta dada, en cambio, la representacion en punto flotante es LA MISMA en ambos casos. – Luis Masuelli Dec 07 '15 at 14:44
13

Cuando representas cualquier Número de punto flotante con doble precisión IEEE754 internamente se tiene algo como:

0111 1111 1111 2222
2222 2222 2222 2222
2222 2222 2222 2222
2222 2222 2222 2222

Donde (0) es bit de signo (0=positivo, 1=negativo), (1) los bits de exponente, y (2) los bits de matisa.

Si en JavaScript comparas 0.5 == 5 * 0.1 vas a obtener true incluso cuando esa operación tiene la imprecisión característica de los puntos flotantes (es decir: algo de error vas a tener). Muchos lenguajes (JavaScript entre ellos) toleran un cierto error en el último bit de una comparación en la mantisa (claro, a mismo exponente y mismo signo).

Lo normal es que el esquema de punto flotante guarda el exponente como un número entre 1024 y -1023 (o al menos así debe entenderse), mientras que la mantisa debe entenderse como un número entre 0 y 0.111111... para dar un número como 0.1b * 2 ^ 12 lo que equivale a calcular 0.5 * 4096 en base decimal. La mantisa, en este sentido, siempre va a ser menor a 1 y se va a guardar de tal forma que el primer dígito (en base binaria) es un 1. Para que esto ocurra, se hace un proceso llamado normalización en el cual corremos el exponente tantos números necesarios como "comas" queramos mover en el número hasta poder armar una mantisa cuyo primer dígito fraccionario en binario sea un 1 (lo cual algunas veces no se puede hacer, y se refleja eso viendo que el exponente, en binario, es 000 0000 0000 y no se puede "bajar" más). En este sentido, siempre que el exponente no sea 000 0000 0000 el número se habrá normalizado para que la mantisa empiece con 0.1 (binario) y, en este sentido, es redundante (ineficiente) almacenar ese 1 (el 0 antes del punto nunca se almacena). Por lo tanto, nuestra mantisa siempre nos permitirá guardar en dígito más y, por esto, podemos llegar a representar 2^53 exactamente (en lugar de 2^52 por tener 52 dígitos en la mantisa).

Veamos ahora que el número 0xFF... tiene 13 letras F o, en binario, 52 bits de longitud. Así nomás se nos ocurre que el número podría guardarse como (positivo)(+52)(1111... 52 "unos") pero en realidad por este "corrimiento" del 1 que viene "implícito", el primer 1 no se guarda. El número pasa a guardarse como (haciendo el corrimiento para el exponente en binario, por supuesto):

0100 0011 0010 1111
1111 1111 1111 1111
1111 1111 1111 1111
1111 1111 1111 1110

(el exponente 1075 es, en realidad, el 52, pero sumado el límite para los exponentes negativos, que es 1023)

El otro número es un uno seguido de 52 ceros. Normalmente sería: (positivo)(+53)(10000... en total 51 "ceros"). Nuevamente como la mantisa empieza con 0.1 (porque podemos normalizar estos números) nos ahorramos el primer uno.

0100 0011 0100 0000
0000 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0000

(el exponente 1076 en realidad es 53, por el mismo motivo)

Ahora hay que comparar los números en igualdad de condiciones. Primero nos aseguramos de que, de hecho, el signo y el exponente sean el mismo. La ALU directamente hace esto, corriendo al mismo tiempo índices y mantisas. Luego, compara las mantisas y ve si son iguales.

Entonces la mantisa de 100000... y 011111... (con los respectivos unos implicitos) van a "parecerse" y tener ese pequeño "epsilon" de diferencia, por lo que van a poder compararse como iguales.

Nota acerca de la mantisa y la representación en punto flotante: Conceptualmente la mantisa es siempre menor a 1. Si se quiere representar un número mayor, se debe concebir usando exponentes. Ejemplos:

  • 0.5 se representa como 0.5 * 2 ^ 0 (considerando el orden correcto de precedencia de los operadores en matemáticas).
  • 1 no se representa como 1 * 2 ^ 0 ya que la mantisa es estrictamente menor a 1, por lo que debe representarse como 0.5 * 2 ^ 1.
  • 65, que en representación binaria es 1000001, será guardado como (65/128) * 2 ^ 7.

Estos números se representarán de la siguiente manera (se debe recordar: como los números son normalizables, el primer "1" es implícito):

0011 1111 1111 0000
0000 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0000

(el exponente 1023 corresponde a 0, con una mantisa con valor 0.1 siendo el 1 implícito).

0100 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0000

(el exponente 1024 corresponde a 1, con una mantisa con valor 0.1 siendo el primer 1 implícito).

y

0100 0000 0110 0000
0100 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0000    

(el exponente 1030 corresponde a 7, con una mantisa que en binario se expresaría 0.1000001, y como el primer 1 es implícito, se guarda como 0000 0100 0000...)

Nota Acerca de los exponentes: Una precisión de más posiciones decimales puede lograrse usando exponentes negativos (entre -1 y -1023): Los exponentes pueden verse como si fueran números positivos, pero en realidad tienen un "desplazamiento" (llamado bias, originalmente) de 1023 (esto quiere decir que el exponente que parece 000 0000 0001 en realidad corresponde a 2^(-1022)). Traduciendolo a potencias en base 10, el menor exponente posible es -308 (considerando también dónde queda la mantisa, porque el número es uno no normalizado). El menor número positivo resulta ser:

0000 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0001

que es: (1 * 2^-52) * 2^-1023 dado el -52 por la mantisa (que recien tiene un 1 al final) y el -1023 por el exponente. El último uno resulta en la posición: 1 * 2^(-1075), que se aproxima al dichoso 10 ^ -308.

El exponente más bajo es 000 0000 0000 correspondiente a (-1023). Sin embargo en este valor las mantisas no tienen implícito el primer 1. Por otro lado, aunque el exponente más grande se puede representar como 111 1111 1111, en realidad no se usa como exponente sino para representar pseudonúmeros de punto flotante comunes:

0111 1111 1111 0000
0000 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0000

corresponde a +Infinity, mientras:

1111 1111 1111 0000
0000 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0000

corresesponde a -Infinity, y cualquier patrón con una mantisa distinta de 0, como:

?111 1111 1111 0000
0000 0000 0000 0000
0000 0000 0000 0000
0000 0000 0000 0001

corresponde to NaN (not a number; representación ideal para resultados que no pueden dar números, como log(-1) o 0/0). El primer bit (el de signo) es irrelevante en estos casos.

Luis Masuelli
  • 484
  • 4
  • 12