Fechas: Date y por qué hoy se usa Intl#
JavaScript representa un instante con el objeto Date. Puedes crear uno con la fecha actual,
desde un texto ISO o con números:
// el instante actual
const ahora = new Date();
// desde un texto ISO (solo-fecha: se interpreta como UTC a medianoche,
// lo que puede mostrar el día anterior en husos horarios negativos)
const desdeIso = new Date("2024-03-15");
// año, mes, día… ¡con una trampa!
const conNumeros = new Date(2024, 2, 15);La trampa clásica de Date: cuando lo construyes con números, el mes es 0-based. Enero es 0,
febrero 1, marzo 2… Por eso new Date(2024, 2, 15) es el 15 de marzo, no de febrero:
// El mes va de 0 (enero) a 11 (diciembre). El día y el año son normales.
// 2 = MARZO
const fichaje = new Date(2024, 2, 15);
// getMonth devuelve el mes 0-based: 2 = marzo
console.log(fichaje.getMonth());
// -> 2
// getFullYear devuelve el año completo
console.log(fichaje.getFullYear());
// -> 2024La otra trampa viene de los strings ISO. new Date("2024-03-15") (solo fecha, sin hora) se
interpreta como UTC a medianoche, no en la hora local. Si tu navegador está en un huso negativo
(p. ej. UTC-5), ese instante cae el día 14 en hora local. Para evitarlo, construye la fecha con
números: new Date(2024, 2, 15) usa medianoche local.
Para convertir un string ISO a Date de forma segura, parte la cadena:
// "2024-03-15" -> [2024, 3, 15] -> new Date con números (medianoche local)
function aFecha(iso) {
// split("-") parte el string en un array de tres trozos
const partes = iso.split("-").map(Number);
// partes[1] es el mes del calendario (1-12); Date lo quiere 0-based
return new Date(partes[0], partes[1] - 1, partes[2]);
}Lo que no debes hacer es montar el texto de la fecha a mano (dia + "/" + mes + "/" + anio): eso
ignora el idioma del usuario, los nombres de los meses y mil detalles.
Mostrar fechas con Intl.DateTimeFormat#
Para mostrar una fecha está Intl.DateTimeFormat, que la formatea según un locale (el
idioma y región, como "es-ES" o "en-US"):
// fecha de fichaje
const fecha = new Date(2024, 2, 15);
// dateStyle 'long' pide la versión larga; el locale decide el idioma y el orden
console.log(new Intl.DateTimeFormat("es-ES", { dateStyle: "long" }).format(fecha));
// -> "15 de marzo de 2024"
console.log(new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(fecha));
// -> "March 15, 2024"El mismo formato, dos salidas distintas. El locale controla el idioma del nombre del mes, el orden
de día/mes/año y el separador. Tú solo describes qué quieres (dateStyle: "long") y el
navegador aplica las reglas del idioma.
Formatear números con Intl.NumberFormat#
El mismo objeto Intl tiene Intl.NumberFormat para porcentajes, monedas y decimales:
// un winrate de 0 a 1
const winrate = 0.652;
// style 'percent' multiplica por 100 y coloca el % según el locale
console.log(new Intl.NumberFormat("es-ES", { style: "percent", maximumFractionDigits: 1 }).format(winrate));
// -> "65,2 %" (coma decimal y espacio antes del %, como en español)
console.log(new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 1 }).format(winrate));
// -> "65.2%" (punto decimal, sin espacio)Y para dinero:
// style 'currency' formatea moneda; necesita la divisa
console.log(new Intl.NumberFormat("es-ES", { style: "currency", currency: "EUR" }).format(1999.9));
// -> "1999,90 €"El navegador ya conoce dónde va el símbolo de divisa, cuántos decimales son convención, si la coma o el punto separan los decimales… Nada de eso lo decides tú.
Reutilizar los formateadores#
Crear un Intl.NumberFormat o un Intl.DateTimeFormat tiene un coste: el motor carga las reglas de
internacionalización del locale. Si los creas dentro de un .map() con 50 héroes, pagas ese coste 50
veces. La solución es crearlos una sola vez y llamar a .format() en cada iteración:
// se crea UNA vez, fuera del bucle
const fmtPorcentaje = new Intl.NumberFormat("es-ES", {
style: "percent",
maximumFractionDigits: 1,
});
// .format() es barato: solo aplica las reglas ya cargadas
const filas = EQUIPO.map((heroe) => {
// reutiliza el mismo formateador
return fmtPorcentaje.format(heroe.winrate);
});Apunte de futuro: manipular fechas con
Date(sumar días, comparar) es incómodo y lleno de trampas. Hay un sustituto moderno,Temporal, que lo arregla; a mediados de 2026 ya está disponible en los navegadores modernos (Chrome, Firefox, Safari recientes), aunque si necesitas soportar versiones antiguas conviene verificar compatibilidad antes de usarlo. Por ahora lo normal en proyectos con soporte amplio esDatepara guardar instantes eIntlpara mostrarlos.
Pruébalo tú#
Dos locales, la misma fecha y el mismo winrate. Cambia "es-ES" por "en-US" en las llamadas a
Intl.DateTimeFormat y a Intl.NumberFormat y pulsa Ejecutar (o Ctrl+Enter) para observar
cómo cambia el formato sin que toques los datos.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué fecha representa `new Date(2024, 2, 15)`?
Tu turno#
Toma el roster en bruto y conviértelo en un informe presentable: formatea winrates y fechas como los espera un usuario español. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en cómo el nivel Excelente crea los formateadores una sola vez y parametriza el locale.
Ejercicio · en esta página
Formatea el informe del equipo según el idioma
Recibes el roster en bruto: winrates de 0 a 1 y fechas en formato ISO. Móntalo en un informe presentable formateando winrates y fechas como los espera un usuario español.
Paso 1: Que funcione
- Muestras una fila por héroe con su nombre, rol, winrate y fecha.
- Vale formatear el winrate a mano (concatenando '%').
- Vale formatear la fecha a mano (recolocando los trozos del ISO).
Paso 2: Que esté pulido
- Formateas el winrate con Intl.NumberFormat (porcentaje, es-ES).
- Formateas la fecha con Intl.DateTimeFormat (dateStyle 'long', es-ES).
- Conviertes el string ISO a Date con el constructor numérico (sin trampa UTC).
Paso 3: Que sea excelente
- Creas los formateadores Intl una sola vez, no uno por fila.
- El locale está parametrizado: crearInforme(EQUIPO, 'es-ES').
- Funciones puras de formateo separadas del render del DOM.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Formatea todo A MANO: el winrate multiplicando por 100 y pegando un '%',
// la fecha recolocando los trozos del string ISO con split("-").
// El informe sale, pero...
// - el porcentaje va en formato fijo, sin respetar el idioma: un español
// espera "65,2 %" (coma decimal, espacio antes del %), no "65.2%".
// - la fecha queda como "15/03/2024": funciona, pero pierde el nombre del
// mes y el tono natural del idioma ("15 de marzo de 2024").
// - tanto el formateo del porcentaje como el de la fecha son fragmentos
// de código que la librería estándar ya resuelve por ti.
// ════════════════════════════════════════════════════════════════════════════
// Copia local del roster para que esta solución sea autocontenida.
const EQUIPO = [
{ nombre: "Tracer", rol: "DPS", winrate: 0.652, fichaje: "2024-03-15" },
{ nombre: "Mercy", rol: "Soporte", winrate: 0.681, fichaje: "2023-11-02" },
{ nombre: "Reinhardt", rol: "Tanque", winrate: 0.54, fichaje: "2024-01-20" },
{ nombre: "Genji", rol: "DPS", winrate: 0.477, fichaje: "2025-02-10" },
{ nombre: "Ana", rol: "Soporte", winrate: 0.633, fichaje: "2024-09-08" },
];
// Winrate (0-1) a porcentaje a mano: multiplico por 100, redondeo a un
// decimal y pego el '%'. Sale con punto decimal (formato inglés).
// 0.652 -> "65.2%"
function porcentajeAMano(winrate) {
return (winrate * 100).toFixed(1) + "%";
}
// Fecha ISO a "DD/MM/AAAA", recolocando los trozos del string.
// Solo manipulación de texto: atado a este formato exacto, sin idioma.
function fechaAMano(iso) {
// "2024-03-15" -> ["2024", "03", "15"]
const partes = iso.split("-");
// -> "15/03/2024"
return partes[2] + "/" + partes[1] + "/" + partes[0];
}
const lineas = EQUIPO.map((heroe) => {
return (
heroe.nombre +
" | " +
heroe.rol +
" | " +
porcentajeAMano(heroe.winrate) +
" | " +
fechaAMano(heroe.fichaje)
);
});
function mostrar(lineas) {
console.log("=== Informe del equipo ===");
for (var i = 0; i < lineas.length; i++) {
console.log(lineas[i]);
}
}
mostrar(lineas); Por qué este nivel
- Formatea a mano: el winrate con (w*100).toFixed(1)+'%' y la fecha recolocando los trozos del string ISO. Funciona, pero sale en formato inglés (65.2%) dentro de una app en español.
- La fecha queda como '15/03/2024': útil, pero sin el nombre del mes ni el tono natural del idioma ('15 de marzo de 2024').
- Es justo el trabajo que Intl ya hace por ti: código frágil para algo que la librería estándar resuelve.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - Intl.NumberFormat con style "percent" formatea el winrate respetando
// el idioma: en es-ES sale "65,2 %" (coma decimal, espacio antes del %),
// no "65.2%". El navegador sabe las reglas; tú solo describes qué quieres.
// - Intl.DateTimeFormat con dateStyle "long" formatea la fecha en palabras y
// en el idioma correcto: "15 de marzo de 2024", sin recolocar trozos a mano.
// - Para pasar de la fecha ISO a un objeto Date, usamos new Date con números
// (el constructor con numeros usa medianoche local, sin el desajuste UTC).
//
// Su límite respecto a Excelente: crea un Intl.NumberFormat y un
// Intl.DateTimeFormat NUEVOS en cada fila (crearlos tiene un coste),
// y el locale "es-ES" aparece repetido por todas partes.
// ════════════════════════════════════════════════════════════════════════════
// Copia local del roster para que esta solución sea autocontenida.
const EQUIPO = [
{ nombre: "Tracer", rol: "DPS", winrate: 0.652, fichaje: "2024-03-15" },
{ nombre: "Mercy", rol: "Soporte", winrate: 0.681, fichaje: "2023-11-02" },
{ nombre: "Reinhardt", rol: "Tanque", winrate: 0.54, fichaje: "2024-01-20" },
{ nombre: "Genji", rol: "DPS", winrate: 0.477, fichaje: "2025-02-10" },
{ nombre: "Ana", rol: "Soporte", winrate: 0.633, fichaje: "2024-09-08" },
];
// Convierte la fecha ISO "AAAA-MM-DD" en un objeto Date de medianoche LOCAL.
// OJO con Date: el mes va 0-based (enero = 0), por eso restamos 1 al mes.
function aFecha(iso) {
// ["2024","03","15"] -> números
const partes = iso.split("-").map(Number);
// mes - 1 porque enero es 0, no 1
return new Date(partes[0], partes[1] - 1, partes[2]);
}
const lineas = EQUIPO.map((heroe) => {
// Intl.NumberFormat con style "percent": multiplica por 100 y coloca el %
// según el idioma. maximumFractionDigits limita los decimales.
// 0.652 -> "65,2 %"
const winrate = new Intl.NumberFormat("es-ES", {
style: "percent",
maximumFractionDigits: 1,
}).format(heroe.winrate);
// Intl.DateTimeFormat con dateStyle "long": fecha en palabras y en el idioma.
// -> "15 de marzo de 2024"
const fecha = new Intl.DateTimeFormat("es-ES", {
dateStyle: "long",
}).format(aFecha(heroe.fichaje));
return heroe.nombre + " | " + heroe.rol + " | " + winrate + " | " + fecha;
});
function mostrar(lineas) {
console.log("=== Informe del equipo ===");
for (var i = 0; i < lineas.length; i++) {
console.log(lineas[i]);
}
}
mostrar(lineas); Por qué es mejor que el anterior
- Intl.NumberFormat con style 'percent' formatea el winrate según el idioma (65,2 %); Intl.DateTimeFormat con dateStyle 'long' da la fecha en palabras (15 de marzo de 2024).
- El navegador pone los decimales, el símbolo %, los nombres de mes: tú solo describes qué quieres y en qué locale.
- Su límite: crea formateadores nuevos en cada fila (caro) y el locale 'es-ES' aparece repetido por todas partes.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - Los formateadores Intl se crean UNA sola vez (crearlos tiene un coste)
// y se reutilizan en cada fila, en vez de fabricar uno nuevo por héroe.
// - El locale se PARAMETRIZA: crearInforme(EQUIPO, "es-ES"). Cambiar a
// "en-US" reformatea winrates y fechas sin tocar una línea más.
// - Funciones puras de formateo (entran datos, salen strings, sin tocar
// el DOM): el cálculo está separado de la presentación y se prueba aparte.
//
// Idea de fondo: la librería estándar hace el trabajo de idioma (decimales,
// símbolo de %, nombres de mes) y tú solo la orquestas, una vez y bien.
// ════════════════════════════════════════════════════════════════════════════
// Copia local del roster para que esta solución sea autocontenida.
const EQUIPO = [
{ nombre: "Tracer", rol: "DPS", winrate: 0.652, fichaje: "2024-03-15" },
{ nombre: "Mercy", rol: "Soporte", winrate: 0.681, fichaje: "2023-11-02" },
{ nombre: "Reinhardt", rol: "Tanque", winrate: 0.54, fichaje: "2024-01-20" },
{ nombre: "Genji", rol: "DPS", winrate: 0.477, fichaje: "2025-02-10" },
{ nombre: "Ana", rol: "Soporte", winrate: 0.633, fichaje: "2024-09-08" },
];
// Convierte la fecha ISO "AAAA-MM-DD" en un Date de medianoche local.
// El mes de Date es 0-based (enero = 0): por eso m - 1.
function aFecha(iso) {
const partes = iso.split("-").map(Number);
// partes[1] es el mes del calendario (1-12); Date lo quiere 0-based
return new Date(partes[0], partes[1] - 1, partes[2]);
}
// Fábrica de formateadores: se crean UNA vez por locale y se reutilizan.
function crearFormateadores(locale) {
return {
// style "percent": multiplica por 100 y añade el símbolo % según el idioma
porcentaje: new Intl.NumberFormat(locale, {
style: "percent",
maximumFractionDigits: 1,
}),
// dateStyle "long": fecha en palabras completas y en el idioma del locale
fecha: new Intl.DateTimeFormat(locale, { dateStyle: "long" }),
};
}
// Función pura: monta el texto de una línea a partir del héroe y los formateadores.
function lineaDe(heroe, fmt) {
// fmt.porcentaje.format reutiliza el mismo Intl.NumberFormat ya creado
const winrate = fmt.porcentaje.format(heroe.winrate);
// fmt.fecha.format reutiliza el mismo Intl.DateTimeFormat ya creado
const fecha = fmt.fecha.format(aFecha(heroe.fichaje));
return heroe.nombre + " | " + heroe.rol + " | " + winrate + " | " + fecha;
}
// Orquesta todo: crea los formateadores una vez y monta todas las líneas.
function crearInforme(equipo, locale) {
// los formateadores se crean aquí, una sola vez para todo el equipo
const fmt = crearFormateadores(locale);
return equipo.map((heroe) => lineaDe(heroe, fmt));
}
function mostrar(lineas) {
console.log("=== Informe del equipo ===");
for (var i = 0; i < lineas.length; i++) {
console.log(lineas[i]);
}
}
// El locale vive en UN sitio: cámbialo a "en-US" y todo el informe se adapta.
mostrar(crearInforme(EQUIPO, "es-ES")); Por qué es mejor que el anterior
- Los formateadores Intl se crean UNA vez y se reutilizan en todas las filas; el locale se parametriza (crearInforme(EQUIPO, 'es-ES')): cámbialo a 'en-US' y todo el informe se adapta.
- Funciones puras de formateo separadas de la presentación: el cálculo se prueba aparte y el DOM solo recibe el resultado ya montado.
- crearFormateadores(locale) es el núcleo de la mejora: una fábrica que encapsula la configuración Intl y la entrega lista para reutilizar.