Precisión Matemática en Software Financiero: Evitando Errores de Coma Flotante (IEEE 754) en JavaScript

Por qué 0.1 + 0.2 no es exactamente 0.3 en IEEE 754 y cómo diseñar motores contables exactos en centavos enteros para aplicaciones de presupuestos y planificación financiera.

En el desarrollo de aplicaciones de planificación financiera, control de gastos y presupuestos en la nube, la exactitud aritmética no es negociable: un descuadre de un solo centavo al sumar las transacciones de un mes erosiona de inmediato la confianza del usuario. Sin embargo, cualquier desarrollador que haya ejecutado 0.1 + 0.2 en la consola de JavaScript sabe que el resultado devuelto es 0.30000000000000004.

1. ¿Por qué Ocurre el Error de Redondeo en IEEE 754 de 64 bits?

El tipo Number en JavaScript utiliza el estándar de coma flotante binaria de doble precisión IEEE 754 (64 bits: 1 bit de signo, 11 bits de exponente y 52 bits de mantisa). Así como la fracción $\frac{1}{3}$ no puede escribirse con un número finito de dígitos en base 10 ($0.3333\ldots$), la fracción decimal $\frac{1}{10}$ ($0.1$) es una fracción periódica infinita en base 2 ($0.0001100110011\ldots_2$).

Al truncar esa secuencia infinita a los 52 bits de la mantisa y sumar múltiples montos con centavos, los pequeños errores de representación binaria se acumulan, provocando que una factura de $19.99 + $4.99 + $0.02 muestre decimales fantasma o falle en comparaciones de igualdad estricta (total === 25.00).

2. La Regla de Oro Contable: Aritmética de Enteros en la Unidad Mínima (Centavos)

La solución estándar utilizada en sistemas bancarios y pasarelas de pago consiste en almacenar y operar todos los valores monetarios como números enteros en su subunidad mínima (centavos para USD/EUR/DOP, donde $25.50 se representa internamente como el entero exacto 2550):

// Conversión segura de entrada de usuario a centavos enteros
export function parseAmountToCents(inputString) {
  const cleaned = String(inputString).replace(/[^0-9.-]/g, '');
  const [whole = '0', fraction = ''] = cleaned.split('.');
  const paddedFraction = (fraction + '00').slice(0, 2);
  const sign = cleaned.startsWith('-') ? -1 : 1;
  return sign * (Math.abs(parseInt(whole, 10)) * 100 + parseInt(paddedFraction, 10));
}

// Formateo internacionalizado para la interfaz de usuario
const currencyFormatter = new Intl.NumberFormat('es-DO', {
  style: 'currency',
  currency: 'USD',
  minimumFractionDigits: 2,
});

export function formatCentsToCurrency(cents) {
  return currencyFormatter.format(cents / 100);
}

3. Distribución de Cuotas sin Pérdida del Centavo Residual (Algoritmo de Reparto)

Otro caso clásico en software de presupuestos ocurre al dividir un gasto de $100.00 (10,000 centavos) en 3 cuotas iguales. Si cada cuota se redondea a $33.33 (3,333 centavos), la suma de las 3 cuotas será $99.99, perdiendo 1 centavo.

Mediante aritmética modular (const remainder = totalCents % installments), asignamos la base entera Math.floor(totalCents / installments) a todas las cuotas y sumamos 1 centavo a las primeras remainder cuotas ($33.34, $33.33, $33.33), garantizando por invariante matemática que la suma de las partes sea siempre idéntica al total original.

IX

Escrito por Equipo de Ingeniería Inovaxsoft

El equipo de ingeniería de Inovaxsoft investiga, desarrolla y documenta arquitecturas web de baja latencia, sistemas distribuidos sobre Kubernetes ARM64 y herramientas de productividad como PomoTask y Roomly.