Cómo redondear números al 0.05 más cercano u otro incremento en JavaScript
Aprende cómo redondear monedas y números a incrementos específicos como 0.05 o 0.10 para países sin monedas pequeñas
Introducción
Un cliente en Canadá añade artículos a un carrito de compras por un total de $11.23, pero el cargo final muestra $11.25. Esto ocurre porque Canadá eliminó el centavo en 2013, por lo que las transacciones en efectivo se redondean al 0.05 más cercano. Tu aplicación necesita mostrar precios que coincidan con lo que los clientes realmente pagan.
El redondeo a incrementos específicos resuelve este problema. Puedes redondear números al níquel más cercano (0.05), al décimo (0.10), o a cualquier otro incremento. Esto se aplica al formato de moneda, sistemas de medición y cualquier escenario donde los valores necesiten alinearse con incrementos específicos.
Esta guía te muestra cómo redondear números a incrementos personalizados usando JavaScript, incluyendo tanto la implementación manual como la moderna API Intl.NumberFormat.
Por qué redondear a incrementos específicos
Muchos países han eliminado monedas de pequeña denominación por razones prácticas. Producir y manejar estas monedas cuesta más que su valor nominal. Las transacciones en efectivo en estos países se redondean a la moneda disponible más cercana.
Países que utilizan redondeo de 0.05 para pagos en efectivo:
- Canadá (eliminó el centavo en 2013)
- Australia (eliminó monedas de 1 y 2 centavos)
- Países Bajos (redondea al 0.05 euros más cercano)
- Bélgica (redondea al 0.05 euros más cercano)
- Irlanda (redondea al 0.05 euros más cercano)
- Italia (redondea al 0.05 euros más cercano)
- Finlandia (redondea al 0.05 euros más cercano)
- Suiza (la moneda más pequeña es 0.05 CHF)
Países que utilizan redondeo de 0.10:
- Nueva Zelanda (eliminó la moneda de 5 centavos en 2006)
Estados Unidos planea dejar de producir centavos a principios de 2026, lo que requerirá un redondeo de 0.05 para transacciones en efectivo.
El redondeo en efectivo solo se aplica al total final de la transacción, no a los artículos individuales. Los pagos electrónicos siguen siendo exactos porque no se intercambian monedas físicas.
Más allá de la moneda, podrías necesitar incrementos personalizados para:
- Sistemas de medición con requisitos específicos de precisión
- Cálculos científicos redondeados a la precisión del instrumento
- Controles de interfaz de usuario que se ajustan a valores específicos
- Estrategias de precios que utilizan puntos de precio psicológicos
El concepto matemático detrás del redondeo por incremento
Redondear a un incremento significa encontrar el múltiplo más cercano de ese incremento. La fórmula divide el número por el incremento, redondea al entero más cercano, y luego multiplica de nuevo por el incremento.
roundedValue = Math.round(value / increment) * increment
Por ejemplo, redondear 11.23 al 0.05 más cercano:
- Dividir: 11.23 / 0.05 = 224.6
- Redondear: Math.round(224.6) = 225
- Multiplicar: 225 * 0.05 = 11.25
La división convierte el incremento en pasos de números enteros. Redondear 224.6 a 225 encuentra el paso más cercano. La multiplicación convierte de nuevo a la escala original.
Para 11.27 redondeado al 0.05 más cercano:
- Dividir: 11.27 / 0.05 = 225.4
- Redondear: Math.round(225.4) = 225
- Multiplicar: 225 * 0.05 = 11.25
Tanto 11.23 como 11.27 se redondean a 11.25 porque caen dentro de 0.025 de ese valor. El punto medio 11.25 permanece en 11.25.
Implementando el redondeo por incremento manualmente
Crea una función que redondee cualquier número a un incremento especificado:
function roundToIncrement(value, increment) {
return Math.round(value / increment) * increment;
}
console.log(roundToIncrement(11.23, 0.05)); // 11.25
console.log(roundToIncrement(11.27, 0.05)); // 11.25
console.log(roundToIncrement(11.28, 0.05)); // 11.30
console.log(roundToIncrement(4.94, 0.10)); // 4.90
console.log(roundToIncrement(4.96, 0.10)); // 5.00
Esto funciona para cualquier incremento, no solo para monedas:
console.log(roundToIncrement(17, 5)); // 15
console.log(roundToIncrement(23, 5)); // 25
console.log(roundToIncrement(2.7, 0.5)); // 2.5
La aritmética de punto flotante de JavaScript puede introducir pequeños errores de precisión. Para cálculos de moneda donde la precisión es importante, multiplica los valores por una potencia de 10 para trabajar con enteros:
function roundToIncrementPrecise(value, increment) {
// Encuentra el número de decimales en el incremento
const decimals = (increment.toString().split('.')[1] || '').length;
const multiplier = Math.pow(10, decimals);
// Convierte a enteros
const valueInt = Math.round(value * multiplier);
const incrementInt = Math.round(increment * multiplier);
// Redondea y convierte de vuelta
return Math.round(valueInt / incrementInt) * incrementInt / multiplier;
}
console.log(roundToIncrementPrecise(11.23, 0.05)); // 11.25
Math.round() utiliza el redondeo hacia arriba en caso de empate, donde los valores exactamente a mitad de camino entre dos incrementos se redondean hacia arriba. Algunas aplicaciones necesitan diferentes modos de redondeo:
function roundToIncrementWithMode(value, increment, mode = 'halfUp') {
const divided = value / increment;
let rounded;
switch (mode) {
case 'up':
rounded = Math.ceil(divided);
break;
case 'down':
rounded = Math.floor(divided);
break;
case 'halfDown':
rounded = divided % 1 === 0.5 ? Math.floor(divided) : Math.round(divided);
break;
case 'halfUp':
default:
rounded = Math.round(divided);
break;
}
return rounded * increment;
}
console.log(roundToIncrementWithMode(11.225, 0.05, 'halfUp')); // 11.25
console.log(roundToIncrementWithMode(11.225, 0.05, 'halfDown')); // 11.20
console.log(roundToIncrementWithMode(11.23, 0.05, 'up')); // 11.25
console.log(roundToIncrementWithMode(11.23, 0.05, 'down')); // 11.20
Uso de Intl.NumberFormat con roundingIncrement
Los navegadores modernos admiten la opción roundingIncrement en Intl.NumberFormat, que maneja automáticamente el redondeo por incrementos al formatear números.
Uso básico para el redondeo de cinco centavos del dólar canadiense:
const formatter = new Intl.NumberFormat('en-CA', {
style: 'currency',
currency: 'CAD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
roundingIncrement: 5
});
console.log(formatter.format(11.23)); // CA$11.25
console.log(formatter.format(11.27)); // CA$11.25
console.log(formatter.format(11.28)); // CA$11.30
La opción roundingIncrement acepta solo valores específicos:
- 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000
Para redondeo de 0.05, establece maximumFractionDigits: 2 y roundingIncrement: 5. Para redondeo de 0.10, establece maximumFractionDigits: 1 y roundingIncrement: 1, o mantén maximumFractionDigits: 2 con roundingIncrement: 10.
Redondeo del franco suizo a 0.05:
const chfFormatter = new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
roundingIncrement: 5
});
console.log(chfFormatter.format(8.47)); // CHF 8.45
console.log(chfFormatter.format(8.48)); // CHF 8.50
Redondeo del dólar neozelandés a 0.10:
const nzdFormatter = new Intl.NumberFormat('en-NZ', {
style: 'currency',
currency: 'NZD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
roundingIncrement: 10
});
console.log(nzdFormatter.format(15.94)); // $15.90
console.log(nzdFormatter.format(15.96)); // $16.00
Puedes combinar roundingIncrement con diferentes modos de redondeo:
const formatterUp = new Intl.NumberFormat('en-CA', {
style: 'currency',
currency: 'CAD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
roundingIncrement: 5,
roundingMode: 'ceil'
});
console.log(formatterUp.format(11.21)); // CA$11.25 (redondeado hacia arriba)
Modos de redondeo disponibles:
ceil: redondeo hacia infinito positivofloor: redondeo hacia infinito negativoexpand: redondeo alejándose de cerotrunc: redondeo hacia cerohalfCeil: redondeo de mitad hacia infinito positivohalfFloor: redondeo de mitad hacia infinito negativohalfExpand: redondeo de mitad alejándose de cero (predeterminado)halfTrunc: redondeo de mitad hacia cerohalfEven: redondeo de mitad al par (redondeo bancario)
Restricciones importantes para roundingIncrement:
- Tanto
minimumFractionDigitscomomaximumFractionDigitsdeben tener el mismo valor - No se puede combinar con redondeo de dígitos significativos
- Solo funciona cuando
roundingPriorityesauto(el valor predeterminado)
Comprobar si el navegador admite roundingIncrement:
function supportsRoundingIncrement() {
try {
const formatter = new Intl.NumberFormat('en-US', {
roundingIncrement: 5,
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
const options = formatter.resolvedOptions();
return options.roundingIncrement === 5;
} catch (e) {
return false;
}
}
if (supportsRoundingIncrement()) {
// Usar Intl.NumberFormat con roundingIncrement
} else {
// Recurrir al redondeo manual
}
Ejemplos de implementación práctica
Crear un formateador de moneda que maneja el redondeo de efectivo según la configuración regional:
function createCashFormatter(locale, currency) {
// Define cash rounding rules by currency
const cashRoundingRules = {
'CAD': { increment: 5, digits: 2 },
'AUD': { increment: 5, digits: 2 },
'EUR': { increment: 5, digits: 2 }, // Some eurozone countries
'CHF': { increment: 5, digits: 2 },
'NZD': { increment: 10, digits: 2 }
};
const rule = cashRoundingRules[currency];
if (!rule) {
// No special rounding needed
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
});
}
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
minimumFractionDigits: rule.digits,
maximumFractionDigits: rule.digits,
roundingIncrement: rule.increment
});
}
const cadFormatter = createCashFormatter('en-CA', 'CAD');
console.log(cadFormatter.format(47.83)); // CA$47.85
const usdFormatter = createCashFormatter('en-US', 'USD');
console.log(usdFormatter.format(47.83)); // $47.83 (no rounding)
Manejar pagos tanto en efectivo como con tarjeta:
function formatPaymentAmount(amount, currency, locale, paymentMethod) {
if (paymentMethod === 'cash') {
const formatter = createCashFormatter(locale, currency);
return formatter.format(amount);
} else {
// Electronic payments use exact amounts
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(amount);
}
}
console.log(formatPaymentAmount(11.23, 'CAD', 'en-CA', 'cash')); // CA$11.25
console.log(formatPaymentAmount(11.23, 'CAD', 'en-CA', 'card')); // CA$11.23
Calcular el monto real a cobrar:
function calculateCashTotal(items, currency) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
// Apply cash rounding only to the final total
const cashRoundingRules = {
'CAD': 0.05,
'AUD': 0.05,
'CHF': 0.05,
'NZD': 0.10
};
const increment = cashRoundingRules[currency];
if (!increment) {
return subtotal;
}
return roundToIncrement(subtotal, increment);
}
function roundToIncrement(value, increment) {
return Math.round(value / increment) * increment;
}
const items = [
{ price: 5.99 },
{ price: 3.49 },
{ price: 1.75 }
];
console.log(calculateCashTotal(items, 'CAD')); // 11.25
console.log(calculateCashTotal(items, 'USD')); // 11.23
Utilizar redondeo por incrementos para valores no monetarios:
// Temperature display rounded to nearest 0.5 degrees
function formatTemperature(celsius) {
const rounded = roundToIncrement(celsius, 0.5);
return new Intl.NumberFormat('en-US', {
style: 'unit',
unit: 'celsius',
minimumFractionDigits: 1,
maximumFractionDigits: 1
}).format(rounded);
}
console.log(formatTemperature(22.3)); // 22.5°C
console.log(formatTemperature(22.6)); // 22.5°C
// Slider values snapped to increments
function snapToGrid(value, gridSize) {
return roundToIncrement(value, gridSize);
}
console.log(snapToGrid(47, 5)); // 45
console.log(snapToGrid(53, 5)); // 55
Consideraciones importantes y casos extremos
Aplica el redondeo de efectivo solo al total final de la transacción, no a los artículos individuales. Redondear cada artículo por separado produce un resultado diferente que redondear la suma:
const items = [1.23, 1.23, 1.23];
// Incorrecto: redondear cada artículo
const wrongTotal = items
.map(price => roundToIncrement(price, 0.05))
.reduce((sum, price) => sum + price, 0);
console.log(wrongTotal); // 3.75
// Correcto: redondear el total
const correctTotal = roundToIncrement(
items.reduce((sum, price) => sum + price, 0),
0.05
);
console.log(correctTotal); // 3.70
Muestra los montos redondeados a los usuarios, pero almacena los montos exactos en tu base de datos. Esto preserva la precisión para la contabilidad y te permite cambiar las reglas de redondeo sin perder información:
const transaction = {
items: [
{ id: 1, price: 5.99 },
{ id: 2, price: 3.49 },
{ id: 3, price: 1.75 }
],
subtotal: 11.23, // Almacena el valor exacto
currency: 'CAD',
paymentMethod: 'cash'
};
// Calcula el monto a mostrar
const displayAmount = roundToIncrement(transaction.subtotal, 0.05); // 11.25
Diferentes países que utilizan la misma moneda pueden tener diferentes reglas de redondeo de efectivo. El euro se utiliza en países con y sin redondeo de efectivo. Verifica las regulaciones específicas del país, no solo la moneda.
Los montos negativos se redondean hacia cero para el valor absoluto, luego se aplica el signo negativo:
console.log(roundToIncrement(-11.23, 0.05)); // -11.25
console.log(roundToIncrement(-11.22, 0.05)); // -11.20
Los números muy grandes pueden perder precisión con la aritmética de punto flotante. Para aplicaciones financieras que manejan grandes cantidades, considera usar aritmética de enteros con la unidad monetaria más pequeña:
function roundToIncrementCents(cents, incrementCents) {
return Math.round(cents / incrementCents) * incrementCents;
}
// Trabaja en centavos
const amountCents = 1123; // $11.23
const roundedCents = roundToIncrementCents(amountCents, 5); // 1125
const dollars = roundedCents / 100; // 11.25
Prueba tu implementación de redondeo con casos extremos:
// Casos de prueba para redondeo de 0.05
const testCases = [
{ input: 0.00, expected: 0.00 },
{ input: 0.01, expected: 0.00 },
{ input: 0.02, expected: 0.00 },
{ input: 0.025, expected: 0.05 },
{ input: 0.03, expected: 0.05 },
{ input: 0.99, expected: 1.00 },
{ input: -0.03, expected: -0.05 },
{ input: 11.225, expected: 11.25 }
];
testCases.forEach(({ input, expected }) => {
const result = roundToIncrement(input, 0.05);
console.log(`${input} -> ${result} (expected ${expected})`);
});
La opción roundingIncrement en Intl.NumberFormat es compatible con navegadores modernos. Firefox agregó soporte en la versión 93. Para aplicaciones que soportan navegadores más antiguos, implementa una alternativa:
function formatCurrency(amount, locale, currency, increment) {
try {
const formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
roundingIncrement: increment
});
// Verifica si roundingIncrement fue realmente aplicado
if (formatter.resolvedOptions().roundingIncrement === increment) {
return formatter.format(amount);
}
} catch (e) {
// roundingIncrement no es compatible
}
// Alternativa: redondeo manual
const rounded = roundToIncrement(amount, increment / 100);
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(rounded);
}
Documenta claramente tu comportamiento de redondeo en las interfaces orientadas al usuario. Los usuarios necesitan entender por qué los montos mostrados difieren de los totales de los artículos:
<div class="cart-summary">
<div class="line-item">Subtotal: $11.23</div>
<div class="line-item total">Total (efectivo): $11.25</div>
<div class="note">Pagos en efectivo redondeados al $0.05 más cercano</div>
</div>
Diferentes jurisdicciones tienen diferentes regulaciones sobre quién se beneficia del redondeo (cliente o comerciante) y cómo manejar casos extremos. Consulta las regulaciones locales para tu caso de uso específico.