El 14 de marzo es una festividad no oficial en el mundo de la programación: el Día de Pi (3,14). Es la ocasión perfecta para hablar de cómo manejan los ordenadores los números fraccionarios, por qué los resultados aritméticos a veces parecen inesperados y qué consecuencias puede tener esto en proyectos reales, sobre todo cuando hay dinero de por medio.
El experimento que sorprende a todo principiante
Abre una consola interactiva en cualquier lenguaje de programación y evalúa la expresión más simple posible:
0.1 + 0.2
Dart, JavaScript, Python, Go, C++, Ruby: el resultado es el mismo, 0.30000000000000004. No 0.3, como sugeriría el sentido común, sino un número con una larga cola de ceros y un cuatro al final.
Esto no es un bug de ningún lenguaje concreto ni un error en tu código. Es una propiedad fundamental de cómo el hardware moderno representa los números fraccionarios en memoria.
Cómo almacenan los ordenadores los números fraccionarios
Toda la información de un ordenador se almacena en binario: secuencias de ceros y unos. Para los enteros esto funciona sin fisuras: 5 en binario es 101, 10 es 1010. Todo entero tiene una representación binaria exacta.
Los números fraccionarios son una historia fundamentalmente distinta. Para convertir una fracción decimal a binario multiplicamos repetidamente por 2 y tomamos la parte entera. Veamos qué pasa con 0,1:
0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0 ← empieza el ciclo
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1 ← termina el ciclo
0.2 × 2 = 0.4 → 0 ← el ciclo se repite
...
El resultado es 0,0(0011): una fracción binaria periódica infinita. Es análogo a cómo 1/3 se convierte en el infinito 0,3333... en decimal: es imposible escribirlo de forma exacta con un número finito de dígitos.
El número 0,2 se enfrenta al mismo problema. Ambos números implicados en nuestra suma ya contienen un error de redondeo en el momento en que se guardan en memoria.
El estándar IEEE 754: un compromiso entre precisión y rendimiento
La inmensa mayoría de los procesadores modernos usan el estándar IEEE 754 para la aritmética en coma flotante. El tipo double (doble precisión de 64 bits), que es el que viene por defecto en la mayoría de los lenguajes, se estructura así:
- 1 bit para el signo (positivo o negativo)
- 11 bits para el exponente (el orden de magnitud)
- 52 bits para la mantisa (los dígitos significativos)
52 bits de mantisa se traducen en aproximadamente 15-17 dígitos decimales significativos. Cuando la representación binaria de una fracción es infinita, se trunca en el bit 52. Ese es el momento exacto en que se introduce el error de representación.
Cuando sumas dos números que contienen cada uno un error de redondeo, esos errores se acumulan. Así es como 0.1 + 0.2 se convierte en 0.30000000000000004.
Cuándo el error es aceptable
Para ser justos, en la mayoría de las tareas ese error es completamente despreciable. Hablamos de un error del orden de 10⁻¹⁶: una milbillonésima.
double funciona perfectamente bien en estos dominios:
- Gráficos por ordenador y renderizado: una diferencia en el decimosexto decimal es invisible para el ojo humano
- Simulaciones físicas en videojuegos: los objetos se comportan de forma realista y el error no tiene efecto perceptible
- Computación científica: los resultados se redondean de todos modos a la precisión necesaria
- Estadística y aprendizaje automático: los modelos operan sobre aproximaciones por naturaleza
Cuándo el error se vuelve crítico: los cálculos financieros
La situación cambia drásticamente cuando hay dinero de por medio. En los sistemas financieros hay que contabilizar cada céntimo con exactitud. Un error en el decimosexto decimal puede parecer insignificante para una sola transacción, pero al procesar miles y millones de operaciones esos errores microscópicos de redondeo se acumulan y se convierten en descuadres reales de saldos.
Piensa en un ejemplo concreto. Supongamos que un sistema procesa 100.000 transacciones al día y cada una introduce un error de 0,000000000000004. Al día son 0,0000000004: una cantidad despreciable. Pero multiplícalo por un año, añade operaciones más complejas con multiplicaciones y divisiones, donde los errores crecen mucho más rápido, y acabas con sumas que ni un contable ni un auditor pueden explicar.
Enfoques fiables para manejar importes monetarios
Enfoque uno: unidades menores como enteros
La idea es sencilla: en lugar de guardar un importe en euros o dólares como número fraccionario, se guarda en las unidades monetarias más pequeñas (céntimos, peniques) como entero.
1999 céntimos son siempre exactamente 19,99 €. Sin redondeos, sin sorpresas, sin un 0,000000004 arrastrándose por detrás. La aritmética de enteros en un ordenador es absolutamente precisa.
Este enfoque se usa mucho en los sistemas de pago. Stripe, por ejemplo, maneja todos los importes exclusivamente en las unidades monetarias más pequeñas.
También merece la pena mencionar una alternativa para las APIs: transmitir los importes monetarios como cadenas. En ese caso la interfaz sigue siendo legible para las personas ("19.99" es más claro que 1999), mientras que la responsabilidad de parsear y elegir el tipo numérico adecuado recae en el lado cliente.
Enfoque dos: librerías de precisión arbitraria (BigDecimal)
La segunda opción fiable es usar tipos de datos especializados que almacenan números decimales sin convertirlos a binario:
- Dart: el paquete
decimal - Java:
java.math.BigDecimal - Python: el módulo
decimalde la librería estándar - JavaScript: la propuesta Decimal está en estudio; mientras tanto, librerías como
decimal.js - Go: el paquete
shopspring/decimal
Estos tipos representan los números en forma decimal, evitando por completo las aproximaciones binarias. La expresión 0.1 + 0.2 == 0.3 usando BigDecimal devuelve true, garantizado.
El precio de la precisión es el rendimiento: las operaciones con BigDecimal son bastante más lentas que con double. Ahora bien, en el contexto de los cálculos financieros, donde la corrección importa más que la velocidad, es un compromiso perfectamente aceptable.
Qué enfoque elegir
Ambos enfoques resuelven el problema, pero encajan en situaciones distintas:
| Criterio | Unidades menores (int) | BigDecimal |
|---|---|---|
| Rendimiento | Máximo | Menor |
| Facilidad de implementación | Alta | Media (según el lenguaje) |
| Soporte de unidades fraccionarias | No (solo enteros) | Sí |
| Soporte multidivisa | Requiere conocer los decimales | Funciona de serie |
Para la mayoría de los sistemas de comercio electrónico y de pago, el enfoque de unidades menores es el óptimo. BigDecimal es preferible en escenarios que requieren cálculos fraccionarios intermedios, por ejemplo al calcular intereses, impuestos o conversiones de divisa.
Conclusión
Si tu proyecto maneja importes monetarios, cíñete a una de estas dos reglas:
- Guarda y transmite los importes en unidades menores como enteros (
int) - Usa tipos de precisión arbitraria (
BigDecimaly sus equivalentes)
Usar double para cálculos financieros es deuda técnica que puede no manifestarse durante meses, pero que acabará provocando descuadres cuya causa raíz será extremadamente difícil de diagnosticar.
Elige herramientas acordes a la tarea y presta atención a cómo representa los números en memoria tu lenguaje de programación. Esta es una de esas cosas que conviene saber antes de que se convierta en un incidente en producción.
Lidiamos exactamente con estos compromisos cuando construimos apps que manejan dinero: nuestro trabajo fintech en ExtraETF y nuestra práctica más amplia de desarrollo de apps con Flutter se apoyan en estas reglas.
¡Feliz Día de Pi! 🥧

