Formatear números en JavaScript: toFixed, Intl y separador de miles

Redondear a dos decimales, mostrar 1 234 567,89 a la francesa, una divisa, un porcentaje o «1,4 k»: los métodos nativos para formatear un número en JavaScript, sus trampas y lo que queda de Numeral.js.

Formatear números en JavaScript con la biblioteca Numeral
Respuesta rápida

Para mostrar un número, usa Intl.NumberFormat o su atajo toLocaleString(): (1234.5).toLocaleString('fr-FR', { minimumFractionDigits: 2 }) da «1 234,50», y la opción style: 'currency' añade la divisa. Para redondear sin mostrar, Math.round(n * 100) / 100; toFixed(2) también redondea, pero devuelve una cadena.

Un número se calcula en Number y se muestra como cadena: «1 234,50 €», «25,6 %», «1,4 k visitas». Entre lo uno y lo otro hay que redondear, colocar un separador de miles y elegir la coma o el punto. JavaScript hace todo eso sin ninguna biblioteca gracias a toFixed, toLocaleString y sobre todo Intl.NumberFormat. Todas las salidas de abajo se han ejecutado en Node.js 22.

Redondear un número

Math.round, Math.floor, Math.ceil, Math.trunc

javascript
Math.round(2.5);   // 3   : al más cercano, .5 sube
Math.round(-2.5);  // -2  : hacia +Infinito, no «lejos de cero»
Math.floor(4.7);   // 4   : hacia abajo
Math.floor(-4.2);  // -5
Math.ceil(4.2);    // 5   : hacia arriba
Math.trunc(-4.7);  // -4  : corta la parte decimal, sea cual sea el signo

Estas funciones solo conocen el entero. Para redondear a dos decimales se multiplica, se redondea y se divide:

javascript
Math.round(1.345 * 100) / 100;   // 1.35
Math.round(1.005 * 100) / 100;   // 1    : se esperaba 1.01, ver más abajo

La trampa de los decimales binarios

1.005 no existe en binario: el número almacenado vale 1.00499999… y se redondea, por tanto, a 1.00. No es un fallo de JavaScript, sino del formato IEEE 754, común a todos los lenguajes. Dos remedios:

javascript
// 1. Corregir el error de representación antes de redondear
Math.round((1.005 + Number.EPSILON) * 100) / 100;   // 1.01

// 2. No almacenar nunca decimales: los precios en céntimos enteros
const prixCentimes = 1005;
(prixCentimes / 100).toFixed(2);                    // '10.05'

El segundo es la regla para todo lo que tiene que ver con el dinero: sumar céntimos enteros nunca produce 0.30000000000000004.

toFixed(): redondear para mostrar

javascript
(1234.5678).toFixed(2);  // '1234.57'
(2.5).toFixed(0);        // '3'
(1.005).toFixed(2);      // '1.00'  : la misma trampa binaria que Math.round
(0.1 + 0.2).toFixed(2);  // '0.30'  : justo para esto sirve

typeof (1.5).toFixed(2); // 'string'
(1.5).toFixed(2) + 1;    // '1.501' : concatenación, no suma

toFixed devuelve una cadena. Es perfecto para mostrar y peligroso para calcular: conviértela con Number() si tienes que reutilizar el valor. Y su punto decimal siempre es un punto; para la coma francesa, pasa por toLocaleString.

Separador de miles y formato francés: toLocaleString()

javascript
const n = 1234567.891;

n.toLocaleString('fr-FR');   // '1 234 567,891'
n.toLocaleString('en-US');   // '1,234,567.891'
n.toLocaleString('de-DE');   // '1.234.567,891'

(1234.5).toLocaleString('fr-FR', { minimumFractionDigits: 2 });   // '1 234,50'
(3.14159).toLocaleString('fr-FR', { maximumFractionDigits: 2 });  // '3,14'

Un método, tres convenciones nacionales, ni una línea de código de formateo. Dos precisiones:

  • El separador de miles francés no es un espacio normal, sino un espacio fino inseparable (U+202F). Impide que «1 234» se parta al final de la línea. Si tienes que quitarlo, filtra con /\s/g, no con ' '.
  • Sin argumento de idioma, toLocaleString() toma el del navegador o el del sistema. Para un resultado estable, indica siempre 'fr-FR'.

Intl.NumberFormat: divisa, porcentaje, unidades

toLocaleString es un atajo hacia Intl.NumberFormat. El objeto completo se crea una vez y se reutiliza, algo que cuenta en cuanto formateas una lista o una tabla.

formats.js
const euros = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
euros.format(1234.5);   // '1 234,50 €'

const dollars = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
dollars.format(1234.5); // '$1,234.50'

const pourcent = new Intl.NumberFormat('fr-FR', { style: 'percent', maximumFractionDigits: 1 });
pourcent.format(0.256); // '25,6 %'  : el valor es un ratio, 0.256 y no 25.6

const km = new Intl.NumberFormat('fr-FR', { style: 'unit', unit: 'kilometer' });
km.format(12.5);        // '12,5 km'

const go = new Intl.NumberFormat('fr-FR', { style: 'unit', unit: 'gigabyte', maximumFractionDigits: 2 });
go.format(2.35);        // '2,35 Go'

El formateador de divisa coloca el símbolo donde toca según el idioma (detrás en francés, delante en inglés), elige el número de decimales de la moneda e inserta el espacio inseparable. Otras tantas reglas que ya no hay que programar.

Notación compacta: 1,4 k y 1 M

Mostrar «1 400 visitas» como «1,4 k», o «1 000 000» como «1 M», es una necesidad habitual. La opción notation: 'compact' lo hace, en el idioma que quieras.

javascript
const compact = new Intl.NumberFormat('fr-FR', { notation: 'compact' });
compact.format(1400);       // '1,4 k'
compact.format(1000000);    // '1 M'

new Intl.NumberFormat('en-US', { notation: 'compact' }).format(23000);   // '23K'
new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 3 }).format(1230974);   // '1.231M'

new Intl.NumberFormat('fr-FR', { notation: 'compact', compactDisplay: 'long' }).format(2500000);   // '2,5 millions'

Otras opciones útiles:

javascript
new Intl.NumberFormat('fr-FR', { minimumIntegerDigits: 2 }).format(7);          // '07'
new Intl.NumberFormat('fr-FR', { useGrouping: false }).format(1234567);        // '1234567'
new Intl.NumberFormat('fr-FR', { signDisplay: 'always' }).format(12);          // '+12'
new Intl.NumberFormat('en-US', { notation: 'scientific' }).format(676565765722); // '6.766E11'
new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 2, roundingMode: 'halfExpand' }).format(1.005); // '1,01'

La última línea merece un comentario: Intl.NumberFormat redondea a partir de la representación decimal del número, no de su aproximación binaria. 1.005 sí se convierte en «1,01», donde toFixed daba «1.00». Una razón más para formatear con Intl en vez de a mano.

Ceros iniciales y otras bases

javascript
String(7).padStart(2, '0');   // '07'  : horas, minutos, números de factura
String(7).padStart(3, '0');   // '007'

(255).toString(16);   // 'ff'        : hexadecimal, para un color CSS
(255).toString(2);    // '11111111'  : binario

(123.456).toPrecision(4);   // '123.5'     : número total de cifras significativas
(1234.5678).toExponential(2); // '1.23e+3'

Dos comportamientos de toString() que conviene conocer: por encima de 1021 y por debajo de 10−6, JavaScript cambia a notación científica ((1e21).toString() da '1e+21' y (0.0000001).toString() da '1e-7'). toFixed o Intl.NumberFormat con notation: 'standard' conservan la escritura desarrollada.

La operación inversa

Una cadena formateada no se reconvierte tal cual: Number('1 234,50') vale NaN. Hay que quitar los espacios y sustituir la coma antes de convertir. El procedimiento completo está en Convertir una cadena en número en JavaScript.

¿Y Numeral.js?

La primera versión de este artículo presentaba Numeral.js, una biblioteca con un formato compacto: numeral(50000).format('0,0') para «50,000», '0a' para «23k», '0.00b' para «2.35GB», '0o' para «23rd». Sigue presente en muchos proyectos y su sintaxis es agradable.

javascript
import numeral from 'numeral';

numeral(50000).format('0,0');       // 50,000
numeral(23000).format('0a');        // 23k
numeral(2348895676).format('0.00b'); // 2.35GB

Pero su última versión, la 2.0.6, es de marzo de 2017 y el proyecto ya no se mantiene: las locales están congeladas y el separador francés no es el espacio fino que se espera. Para un proyecto nuevo, Intl.NumberFormat cubre las mismas necesidades (miles, compacto, divisa, porcentaje) sin ninguna dependencia, con las convenciones reales de cada idioma y sin peso en el bundle. Conserva Numeral donde ya está; no lo añadas más.

Resumen

Necesidad Herramienta Ejemplo
Redondear para calcular Math.round(n * 100) / 100 o céntimos enteros 1.35
Redondear para mostrar, punto decimal toFixed(2) '1234.57'
Formato francés, miles y coma toLocaleString('fr-FR') '1 234 567,891'
Divisa, porcentaje, unidad Intl.NumberFormat + style '1 234,50 €'
1,4 k / 1 M notation: 'compact' '1,4 k'
Ceros iniciales padStart(2, '0') '07'

Errores frecuentes

Sumar el resultado de toFixed (1.5).toFixed(2) + 1 da «1.501»: toFixed devuelve una cadena y la suma concatena. Conviértelo con Number() si tienes que volver a calcular.
Esperar un redondeo «de manual» (1.005).toFixed(2) vale «1.00» porque 1.005 se almacena como 1.00499999… en binario. Para un redondeo financiero, trabaja con céntimos enteros.
Sustituir el espacio de toLocaleString('fr-FR') por un espacio normal El separador de miles francés es un espacio fino inseparable (U+202F). Un replace(' ', '') no lo quita; usa /\s/g.
Volver a crear Intl.NumberFormat dentro de un bucle Construir el formateador es costoso. Créalo una sola vez y luego llama a format() con cada valor.

JavaScript

Damien Flandrin Desarrollador web desde 2010, creador de Gekkode y de Email Impact. Cada artículo se prueba en un proyecto real antes de publicarse. Contacto
Newsletter

Las nuevas pruebas, tutoriales y proyectos, por correo.

Pruebas reproducibles, código versionado, resultados fechados. Nunca spam.