Cómo redondear números al 0,05 más cercano u otro incremento en JavaScript
Aprende a 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 que suman $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 dime (0,10) o a cualquier otro incremento. Esto se aplica al formato de monedas, 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 API moderna Intl.NumberFormat.
Por qué redondear a incrementos específicos
Muchos países eliminaron las 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 usan redondeo de 0,05 para pagos en efectivo:
- Canadá (eliminó el centavo en 2013)
- Australia (eliminó las 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 usan 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 de efectivo solo se aplica al total final de la transacción, no a los artículos individuales. Los pagos electrónicos permanecen 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 de precisión específicos
- 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 incrementos
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, redondeando 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. Multiplicar 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.
Implementación manual del redondeo por incrementos
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 moneda:
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 importa, multiplica los valores por una potencia de 10 para trabajar con enteros:
function roundToIncrementPrecise(value, increment) {
// Find the number of decimal places in the increment
const decimals = (increment.toString().split('.')[1] || '').length;
const multiplier = Math.pow(10, decimals);
// Convert to integers
const valueInt = Math.round(value * multiplier);
const incrementInt = Math.round(increment * multiplier);
// Round and convert back
return Math.round(valueInt / incrementInt) * incrementInt / multiplier;
}
console.log(roundToIncrementPrecise(11.23, 0.05)); // 11.25
Math.round() utiliza redondeo half-up, 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 el redondeo por incrementos automáticamente al formatear números.
Uso básico para el redondeo a níquel 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 a 0.05, establece maximumFractionDigits: 2 y roundingIncrement: 5. Para redondeo a 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 (rounded up)
Modos de redondeo disponibles:
ceil: redondear hacia el infinito positivofloor: redondear hacia el infinito negativoexpand: redondear alejándose de cerotrunc: redondear hacia cerohalfCeil: redondear la mitad hacia el infinito positivohalfFloor: redondear la mitad hacia el infinito negativohalfExpand: redondear la mitad alejándose de cero (predeterminado)halfTrunc: redondear la mitad hacia cerohalfEven: redondear la 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 predeterminado)
Verifica 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()) {
// Use Intl.NumberFormat with roundingIncrement
} else {
// Fall back to manual rounding
}
Ejemplos prácticos de implementación
Crea un formateador de moneda que maneje el redondeo en 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)
Gestiona 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
Calcula el importe 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
Usa redondeo por incremento 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 especiales
Aplica el redondeo en 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];
// Wrong: rounding each item
const wrongTotal = items
.map(price => roundToIncrement(price, 0.05))
.reduce((sum, price) => sum + price, 0);
console.log(wrongTotal); // 3.75
// Correct: rounding the total
const correctTotal = roundToIncrement(
items.reduce((sum, price) => sum + price, 0),
0.05
);
console.log(correctTotal); // 3.70
Muestra los importes redondeados a los usuarios, pero almacena los importes 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, // Store exact value
currency: 'CAD',
paymentMethod: 'cash'
};
// Calculate display amount
const displayAmount = roundToIncrement(transaction.subtotal, 0.05); // 11.25
Diferentes países que usan la misma moneda pueden tener reglas de redondeo en efectivo diferentes. El euro se usa en países con y sin redondeo en efectivo. Verifica las regulaciones específicas del país, no solo la moneda.
Los importes negativos se redondean hacia cero para el valor absoluto y 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;
}
// Work in cents
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 especiales:
// Test cases for 0.05 rounding
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 añadió 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
});
// Check if roundingIncrement was actually applied
if (formatter.resolvedOptions().roundingIncrement === increment) {
return formatter.format(amount);
}
} catch (e) {
// roundingIncrement not supported
}
// Fallback: manual rounding
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 de cara al usuario. Los usuarios necesitan entender por qué los importes 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 (cash): $11.25</div>
<div class="note">Cash payments rounded to nearest $0.05</div>
</div>
Diferentes jurisdicciones tienen diferentes regulaciones sobre quién se beneficia del redondeo (cliente o comerciante) y cómo manejar casos especiales. Consulta las regulaciones locales para tu caso de uso específico.