Por qué los números cambian al pasar por un programa
Los números parecen lo más seguro que se puede mover entre sistemas, y son lo que más a menudo llega sutilmente mal. La razón es que el tipo numérico habitual es un doble — una fracción binaria con 53 bits de precisión — y los valores que le importan a la gente son fracciones decimales e identificadores largos, y ese formato no guarda ninguno de los dos con exactitud. Los fallos son silenciosos porque la presentación redondea el daño hasta hacerlo invisible.
Las fracciones decimales no existen en binario
Un tercio no tiene forma decimal exacta: escribes 0,3333 y te detienes en algún punto. En binario le ocurre lo mismo a un décimo. El valor almacenado más próximo a 0.1 es 0.1000000000000000055511151231257827021181583404541015625, y el más próximo a 0.2 también está algo desviado, así que sumarlos aterriza en un valor que se imprime como 0.30000000000000004.
No es un fallo de un lenguaje concreto. Todo entorno que use el mismo formato de 64 bits da la misma respuesta, porque la respuesta es una propiedad del formato. Lo exacto en binario son los enteros y las fracciones cuyo denominador es una potencia de dos, así que un medio y un cuarto se comportan a la perfección y un décimo nunca lo hará.
Por encima de nueve mil billones los enteros dejan de ser consecutivos
Un doble guarda enteros exactos hasta 2 elevado a 53, que es 9007199254740992. El siguiente no se puede representar: pide 9007199254740993 y recibes 9007199254740992, en silencio. A partir de ahí los enteros representables van de dos en dos, luego de cuatro en cuatro, y así sucesivamente.
Ese umbral cae justo en medio de un tipo de dato muy común. Los identificadores de sesenta y cuatro bits — claves de base de datos, identificadores de mensajes y publicaciones de las grandes plataformas, identificadores al estilo snowflake — lo superan de forma rutinaria, así que un identificador que pasa por un doble sale convertido en un número vecino: sigue teniendo forma de identificador, pero no apunta a nada.
JSON es donde suele ocurrir
JSON no limita el tamaño de un número; el formato es texto y todas las cifras están ahí. Quien las pierde es el analizador, porque la mayoría convierte cada número en un doble según lo lee. La corrupción ocurre por tanto en el lado receptor, y los registros del emisor se ven impecables.
La regla práctica es transportar los identificadores grandes como cadenas. Un identificador no es una cantidad — nunca sumas dos — así que no se pierde nada al entrecomillarlo, y entrecomillar es lo único que sobrevive a una ida y vuelta por analizadores arbitrarios. Si no puedes cambiar el mensaje, compara al menos el identificador recibido con el texto original y no con el número ya analizado.
El dinero es el otro sitio donde muerde
Los precios son fracciones decimales, así que heredan todos los problemas anteriores: un décimo no es exacto, tres décimos no son tres veces un décimo, y una suma larga se desvía por fracciones de céntimo que acaban redondeando a una diferencia visible. Guarda el dinero como un número entero de la unidad más pequeña, o en un tipo decimal pensado para ello.
El redondeo es una decisión aparte que la gente da por universal, y no lo es. Redondear el medio hacia arriba y redondear el medio al par más cercano tienen ambos una larga historia contable, y distintos lenguajes y hojas de cálculo eligen distintos valores por defecto: así es como dos sistemas que coinciden en todas las entradas discrepan en un total.
La presentación esconde el daño
Al imprimir un doble se muestra normalmente el texto más corto que vuelve a leerse como el mismo valor, así que un número ligeramente equivocado suele imprimirse como el valor limpio que esperabas. Dos números pueden verse idénticos y aun así fallar una comparación de igualdad, que es justo la situación que hace que este tipo de error parezca sobrenatural.
Compara entonces en el mismo terreno. Para ver el valor realmente almacenado, imprímelo con diecisiete cifras significativas o conviértelo a decimal; y para comparar dos valores calculados, admite una tolerancia proporcional a su magnitud en vez de exigir igualdad exacta.
Las idas y vueltas pierden cifras si no escribes suficientes
Convertir un doble a texto y de vuelta solo es exacto si el texto lleva diecisiete cifras significativas; para un flotante de 32 bits la cifra es nueve. Cualquier cosa que formatee a seis decimales por legibilidad — una línea de registro, una exportación a CSV, un archivo de configuración — ha tirado parte del valor, y al leerlo se obtiene un número distinto del de partida.
Lo mismo vale para reducir la precisión a propósito. Un flotante de 32 bits guarda unas siete cifras significativas, así que pasar un doble por él y volver deja 0.1 como 0.100000001490116119384765625. Eso está bien para una coordenada en un videojuego y no está bien para una medida que vas a sumar un millón de veces.
Qué comprobar cuando un número parece mal
Prueba los límites a propósito en lugar de esperar a llevarte la sorpresa. Manda 0.1 más 0.2 por el camino y mira si aparecen las cifras de más; manda 9007199254740993 y mira si vuelve igual; manda un valor con más de diecisiete cifras significativas y mira qué sobrevive.
Cuando algo no cuadre, averigua qué paso perdió la información: la presentación, el analizador, una conversión a flotante o la propia aritmética. Cada uno tiene un arreglo distinto — imprimir más cifras, entrecomillar el identificador, usar un tipo más ancho o pasarse a enteros — y aplicar el equivocado deja el problema donde estaba y encima más difícil de ver.