Manejo de Dinero y Moneda
Cómo representar, analizar, formatear y calcular valores monetarios con precisión entre monedas.
Visión General
Los cálculos financieros exigen precisión. Los números de punto flotante (float, double) no representan con exactitud la mayoría de las fracciones decimales, así que los errores de redondeo se acumulan en sistemas de facturación, cobro y trading. Abajo vas a representar dinero como valores decimales exactos, formatearlo con símbolos según la región, operar sin perder precisión y convertir entre monedas — en Python, JavaScript y Java.
Cuándo Usar
Saca esto a relucir cuando estés:
- Construyendo carritos de e-commerce, flujos de pago o sistemas de facturación. Consulta Validación de Entrada para sanitizar datos de órdenes.
- Agregando transacciones financieras donde la precisión al nivel del centavo importa.
- Mostrando precios en USD, EUR o JPY con reglas de redondeo correctas.
- Convirtiendo entre monedas con APIs externas de tasas de cambio.
Solución
Python
from decimal import Decimal, ROUND_HALF_UP, getcontext
# Establecer precisión para todas las operaciones Decimal
getcontext().prec = 28
class Money:
def __init__(self, amount: str | Decimal, currency: str = "USD"):
self.amount = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
self.currency = currency.upper()
def __add__(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("No se pueden sumar monedas distintas sin conversión")
return Money(self.amount + other.amount, self.currency)
def __mul__(self, factor: Decimal) -> "Money":
return Money(self.amount * Decimal(str(factor)), self.currency)
def format(self) -> str:
symbol = {"USD": "$", "EUR": "€", "GBP": "£"}.get(self.currency, self.currency)
return f"{symbol}{self.amount:,}"
@staticmethod
def convert(amount: "Money", rate: Decimal, target_currency: str) -> "Money":
converted = amount.amount * Decimal(str(rate))
return Money(converted, target_currency)
# Uso
price = Money("19.99")
tax = Money("1.70")
total = price + tax
converted = Money.convert(total, Decimal("0.85"), "EUR")
print(total.format()) # $21.69
print(converted.format()) # €18.44
JavaScript
Dinero.js v2 expone una API funcional — los montos viven en unidades menores (centavos), así que 1999 significa $19.99:
import { dinero, add, toFormat, convert } from "dinero.js";
import { USD, EUR } from "@dinero.js/currencies";
const price = dinero({ amount: 1999, currency: USD });
const tax = dinero({ amount: 170, currency: USD });
const total = add(price, tax);
// Conversión con tasa explícita: 1 USD = 0.8500 EUR
const converted = convert(total, EUR, {
EUR: { amount: 8500, scale: 4 },
});
const print = (d) =>
toFormat(d, ({ amount, currency }) => `${currency.code} ${amount.toFixed(2)}`);
console.log(print(total)); // USD 21.69
console.log(print(converted)); // EUR 18.44
Java
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Currency;
import java.util.Locale;
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, String currencyCode) {
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = Currency.getInstance(currencyCode);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Moneda incompatible");
}
return new Money(this.amount.add(other.amount), currency.getCurrencyCode());
}
public Money multiply(BigDecimal factor) {
return new Money(this.amount.multiply(factor), currency.getCurrencyCode());
}
public String format(Locale locale) {
NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
formatter.setCurrency(currency);
return formatter.format(amount);
}
public static Money convert(Money source, BigDecimal rate, String targetCurrency) {
BigDecimal converted = source.amount.multiply(rate);
return new Money(converted, targetCurrency);
}
// Uso
public static void main(String[] args) {
Money price = new Money(new BigDecimal("19.99"), "USD");
Money tax = new Money(new BigDecimal("1.70"), "USD");
Money total = price.add(tax);
Money eur = Money.convert(total, new BigDecimal("0.85"), "EUR");
System.out.println(total.format(Locale.US)); // $21.69
System.out.println(eur.format(Locale.GERMANY)); // 18,44 €
}
}
Explicación
- Nunca uses
floatodoublepara dinero — el punto flotante binario no representa exactamente valores como0.1, lo que produce errores tipo0.1 + 0.2 != 0.3que se acumulan en sistemas financieros. - Usa unidades menores enteras (centavos) o decimales de precisión arbitraria (
Decimal,BigDecimal, Dinero.js) para mantener valores exactos durante todo el cálculo. - El redondeo debe ocurrir explícitamente en límites definidos (por línea de ítem, por impuesto y por total), nunca de forma implícita por error de punto flotante.
- La conversión de moneda debe usar una tasa de cambio obtenida, aplicarla con aritmética exacta y luego redondear a las unidades menores de la moneda destino.
- El formato por región separa la lógica de presentación del almacenamiento: guarda valores numéricos crudos y formatea con
NumberFormato bibliotecas ICU para símbolos, separadores y posición. - Dividir dinero requiere una estrategia de reparto — dividir $100 entre tres da $33.33/$33.33/$33.34, no tres partes iguales.
allocatede Dinero.js ydivideAndRemainderde Java reparten los centavos sobrantes de forma determinista; la división ingenua los pierde.
Toda operación con dinero sigue el mismo pipeline: parsear, validar, calcular, convertir, redondear, formatear:
Cómo Trabajar con Tasas de Cambio
Las tasas cambian constantemente, así que el código de conversión importa menos que la política que lo rodea:
- Obtén la tasa una vez por transacción — consúltala en tu proveedor de tasas y reutiliza esa misma tasa para cada línea del pedido; nunca la vuelvas a pedir a mitad del cálculo.
- Guarda la tasa que usaste — registra
rate,rate_timestampytarget_amounten la transacción. Meses después, soporte o un auditor preguntarán “¿por qué €18.44?”. - Cachea con TTL — la mayoría de proveedores actualizan a diario; una tasa cacheada con unas horas de TTL sirve para una tienda, pero no para liquidaciones.
- Redondea solo después de convertir —
21.69 × 0.85 = 18.4365se convierte en18.44al final, nunca redondeando pasos intermedios.
Variantes
| Enfoque | Biblioteca / Tipo | Ideal Para |
|---|---|---|
| Centavos enteros | long / bigint | Caminos de alta frecuencia, menor huella de memoria |
| Precisión arbitraria | Decimal (Python), BigDecimal (Java) | Matemática decimal exacta de propósito general |
| Biblioteca de dinero | Dinero.js, Money PHP, JSR-354 (Java) | APIs ricas de formato, reparto y comparación |
| Almacenamiento en DB | DECIMAL(19,4) (SQL), NUMERIC (PostgreSQL) | Valores exactos persistentes con 4 decimales para tasas |
Lo que Funciona
- Almacena montos en unidades menores (centavos) o tipos decimales exactos; nunca guardes dinero como punto flotante en bases de datos.
- Redondea en el límite correcto — calcula líneas de ítem con precisión completa, redondea por línea y luego suma los valores redondeados para el total.
- Separa dinero de presentación — mantén
Decimal/BigDecimal/ centavos internamente y formatea solo en la capa de UI o API. - Conoce tu modo de redondeo —
HALF_UPes el estándar comercial;HALF_EVEN(redondeo bancario, el predeterminado de Python) reduce el sesgo en agregaciones grandes. Elige uno por dominio y deja anotado cuál — los auditores siempre preguntan. - Cachea las tasas de cambio con TTL y timestamp; convierte siempre con la tasa vigente al momento de la transacción.
Errores Comunes
- Agarrar
floatodoublepara precios — así es como0.30000000000000004termina en una factura. - Redondear solo al final de una cadena larga de cálculos, propagando errores sub-centavo.
- Guardar dinero como strings y parsear con comas/puntos dependientes de la región, causando errores regionales.
- Sumar monedas distintas directamente sin conversión, produciendo totales sin sentido.
- Ignorar las subunidades de moneda — algunas monedas (JPY, KRW) no tienen decimales; otras (BHD, IQD) tienen 3.
Cuándo No Usarlo
- Estimaciones solo visuales — porcentajes, barras de progreso y proyecciones aproximadas donde nadie concilia el último centavo toleran bien
float. - Prototipos y herramientas internas — si ningún pago real toca el número, la ceremonia de un tipo Money aporta poco.
- Cuando tu framework ya incluye un tipo de dinero — los campos money de Django,
money-rails, las implementaciones de JSR-354 — úsalo. Una clase hecha a mano es tuya de mantener para siempre.
Cómo Probar Código de Dinero
Los errores de dinero se esconden en los límites, así que prueba los bordes y no el camino feliz:
- Límites de redondeo —
19.995redondea a20.00con HALF_UP;2.5redondea a2con HALF_EVEN. Escribe un test para el modo que elegiste, no el que asumiste. - Moneda incompatible — sumar USD a EUR debe lanzar error, no sumar en silencio.
- Ida y vuelta de repartos — dividir
100.00entre tres debe devolver partes que vuelven a sumar100.00. - Casos de unidades menores — JPY (
¥1234, cero decimales) y BHD (tres decimales) rompen código que fijaquantize("0.01"). - Conversiones idempotentes — convertir USD→EUR→USD rara vez devuelve el monto original; verifica la tolerancia documentada en lugar de igualdad.
Lectura Adicional
- Documentación del módulo
decimalde Python — cómo funcionan de verdad los contextos,quantizey los modos de redondeo. - Documentación de Dinero.js — la API funcional de dinero que usa el ejemplo de arriba.
- Especificación JSR-354 (JavaMoney) — el estándar
javax.moneyque implementan la mayoría de bibliotecas Java de dinero. - Códigos de moneda ISO 4217 — donde se define la unidad menor oficial de cada moneda.
Preguntas frecuentes
¿Por qué no puedo usar float o double para dinero?
El punto flotante binario guarda las fracciones como sumas de potencias inversas de 2. Decimales como 0.1 son secuencias infinitas en binario, así que se redondean. Estos errores diminutos se acumulan en multiplicación y división, provocando facturas desviadas por centavos o más.
¿Cómo debería almacenar dinero en una base de datos?
Usa DECIMAL(19,4) (o NUMERIC en PostgreSQL) para preservar 4 decimales de precisión. Esto cubre cálculos sub-centavo (tasas de impuesto, tasas de cambio) manteniendo exacto el valor final de 2 decimales. Evita columnas FLOAT, DOUBLE y REAL.
¿Qué modo de redondeo debería usar?
HALF_UP (redondear 0.5 alejándose de cero) es el estándar en la mayoría de aplicaciones comerciales. HALF_EVEN (redondeo bancario) aparece en algunos estándares financieros y reduce el sesgo en agregaciones grandes. Redondea siempre de forma consistente dentro de tu dominio y documenta el modo para los auditores.
¿Cómo divido un pago sin perder centavos?
Divide en unidades menores y reparte el resto de forma determinista. $100.00 entre tres queda 3333 + 3333 + 3334 centavos. Dinero.js incluye allocate exactamente para esto; con decimales planos, ordena por resto mayor primero para que el mismo reparto produzca siempre las mismas partes.
¿Qué pasa con monedas sin decimales como el JPY?
Guarda el conteo de unidades menores, no una suposición fija de 2 decimales. JPY y KRW no tienen decimales; BHD e IQD tienen tres — ISO 4217 define el exponente por moneda. El código que fija 100 como divisor corrompe valores la primera vez que pasa un precio en ¥.
¿Tipo decimal o centavos enteros — cuál elegir?
Los centavos enteros (1999 para $19.99) son rápidos, fáciles de indexar y portables entre lenguajes, pero cada límite exige disciplina de conversión. Decimal/BigDecimal se lee más natural y maneja matemática sub-unidad (tasas, porcentajes de impuestos) sin pensarlo dos veces. Elige centavos para caminos calientes y ledgers simples; decimales para lógica de negocio general.
Recursos Relacionados
Formateo de Fechas en Python, JavaScript y Java
Cómo parsear, formatear y mostrar fechas entre zonas horarias en Python, JavaScript y Java, con una referencia rápida de directivas por lenguaje.
RecipeValidación de Input: Schemas en 3 Lenguajes
Valida el input de usuarios en el límite de la API con librerías de schemas. Ejemplos con Pydantic, Zod y Jakarta Validation, sanitización y errores estructurados.
RecipeLlamar a una API REST: Python, JS, Java y Go
Cómo hacer peticiones HTTP a una API REST y manejar la respuesta JSON en Python, JavaScript, Java y Go.
RecipeCaché y Memoización en Python, JavaScript y Java
Cómo cachear computaciones costosas y respuestas de API usando caches en memoria, LRU, TTL y distribuidos en Python, JavaScript y Java.
RecipeParsear JSON en Python, JavaScript y Java
Parsea cadenas JSON a objetos nativos en Python, JavaScript y Java — con manejo de errores, streaming para archivos grandes y validación de esquema.