Números grandes redondeados erróneamente en JavaScript

Resuelto Jaanus asked hace 15 años • 6 respuestas

Vea este código:

var jsonString = '{"id":714341252076979033,"type":"FUZZY"}';
var jsonParsed = JSON.parse(jsonString);
console.log(jsonString, jsonParsed);
Expandir fragmento

Cuando veo mi consola en Firefox 3.5, el valor de jsonParsedes el número redondeado:

Object id=714341252076979100 type=FUZZY

Probé diferentes valores, el mismo resultado (número redondeado).

Tampoco entiendo sus reglas de redondeo. 714341252076979136 se redondea a 714341252076979200, mientras que 714341252076979135 se redondea a 714341252076979100.

¿Por qué está pasando esto?

Jaanus avatar Sep 04 '09 22:09 Jaanus
Aceptado

Está desbordando la capacidad del numbertipo de JavaScript; consulte §8.5 de la especificación y la página de Wikipedia sobre punto flotante binario de doble precisión IEEE-754 para obtener más detalles. Esas identificaciones deberán ser cadenas.

El punto flotante de doble precisión IEEE-754 (el tipo de número que usa JavaScript) no puede representar con precisión todos los números (por supuesto). Es famoso que 0.1 + 0.2 === 0.3es falso. Eso puede afectar a los números enteros al igual que afecta a los números fraccionarios; comienza una vez que superas 9,007,199,254,740,991 ( Number.MAX_SAFE_INTEGER).

Más allá de Number.MAX_SAFE_INTEGER + 1( 9007199254740992), el formato de punto flotante IEEE-754 ya no puede representar todos los números enteros consecutivos. 9007199254740991 + 1es 9007199254740992, pero también9007199254740992 + 1 es porque no se puede representar en el formato. Lo siguiente que puede ser es . Entonces no puede ser, pero puede. 90071992547409929007199254740993900719925474099490071992547409959007199254740996

La razón es que nos hemos quedado sin bits, por lo que ya no tenemos bits de unos; el bit de orden más bajo ahora representa múltiplos de 2. Eventualmente, si continuamos, perdemos ese bit y solo trabajamos en múltiplos de 4. Y así sucesivamente.

Sus valores están muy por encima de ese umbral y, por lo tanto, se redondean al valor representable más cercano.

A partir de ES2020, puede usarlo BigIntpara números enteros que sean arbitrariamente grandes, pero no hay representación JSON para ellos. Podrías usar cadenas y una función reviver:

const jsonString = '{"id":"714341252076979033","type":"FUZZY"}';
// Note it's a string −−−−^−−−−−−−−−−−−−−−−−−^

const obj = JSON.parse(jsonString, (key, value) => {
    if (key === "id" && typeof value === "string" && value.match(/^\d+$/)) {
        return BigInt(value);
    }
    return value;
});

console.log(obj);
(Look in the real console, the snippets console doesn't understand BigInt.)
Expandir fragmento


Si tiene curiosidad acerca de los bits, esto es lo que sucede: un número binario de punto flotante de doble precisión IEEE-754 tiene un bit de signo, 11 bits de exponente (que define la escala general del número, como una potencia de 2 [ porque es un formato binario]), y 52 bits de significado (pero el formato es tan inteligente que obtiene 53 bits de precisión de esos 52 bits). La forma en que se usa el exponente es complicada ( se describe aquí ), pero en términos muy vagos, si sumamos uno al exponente, el valor del significado se duplica, ya que el exponente se usa para potencias de 2 (nuevamente, tenga en cuenta que es no es directo, hay inteligencia ahí).

Así que veamos el valor 9007199254740991(también conocido como Number.MAX_SAFE_INTEGER):

   +-------------------------------------------------------- −−−−−−−−−−−−−−− bit de signo
  / +-------+---------------------------------------------- −−−−−−−−−−−−−−− exponente
 / / | +-------------------------------------------------------- +− significativo
/ / | / |
0 10000110011 111111111111111111111111111111111111111111111111111111
                = 9007199254740991 (Número.MAX_SAFE_INTEGER)

Ese valor de exponente, 10000110011significa que cada vez que sumamos uno al significado, el número representado aumenta en 1 (el número entero 1, perdimos la capacidad de representar números fraccionarios mucho antes).

Pero ahora ese significado está lleno. Para pasar de ese número, tenemos que aumentar el exponente, lo que significa que si sumamos uno al significado, el valor del número representado sube en 2, no en 1 (porque el exponente se aplica a 2, la base de este número de punto flotante binario):

   +-------------------------------------------------------- −−−−−−−−−−−−−−− bit de signo
  / +-------+---------------------------------------------- −−−−−−−−−−−−−−− exponente
 / / | +-------------------------------------------------------- +− significativo
/ / | / |
0 10000110100 000000000000000000000000000000000000000000000000000000
                = 9007199254740992 (Número.MAX_SAFE_INTEGER + 1)

Bueno, está bien, porque de todos modos 9007199254740991 + 1lo es 9007199254740992. ¡Pero! No podemos representar 9007199254740993. Nos hemos quedado sin bits. Si sumamos solo 1 al significado, suma 2 al valor:

   +-------------------------------------------------------- −−−−−−−−−−−−−−− bit de signo
  / +-------+---------------------------------------------- −−−−−−−−−−−−−−− exponente
 / / | +-------------------------------------------------------- +− significativo
/ / | / |
0 10000110100 00000000000000000000000000000000000000000000000000001
                = 9007199254740994 (Número.MAX_SAFE_INTEGER + 3)

El formato ya no puede representar números impares. A medida que aumentamos el valor, el exponente es demasiado grande.

Al final, nos quedamos sin bits significativos nuevamente y tenemos que aumentar el exponente, por lo que al final solo podemos representar múltiplos de 4. Luego, múltiplos de 8. Luego, múltiplos de 16. Y así sucesivamente.

T.J. Crowder avatar Sep 04 '2009 15:09 T.J. Crowder

Lo que estás viendo aquí es en realidad el efecto de dos redondeos. Los números en ECMAScript se representan internamente en punto flotante de doble precisión. Cuando idse establece en 714341252076979033( 0x9e9d9958274c359en hexadecimal), en realidad se le asigna el valor de doble precisión representable más cercano, que es 714341252076979072( 0x9e9d9958274c380). Cuando imprime el valor, se redondea a 15 dígitos decimales significativos, lo que da 14341252076979100.

Stephen Canon avatar Sep 04 '2009 15:09 Stephen Canon

Este analizador JSON no lo causa. Simplemente intenta ingresar 714341252076979033a la consola de fbug. Verás lo mismo 714341252076979100. Consulte esta publicación de blog para obtener más detalles: punto flotante

thorn0 avatar Sep 04 '2009 15:09 thorn0