StackPractices
beginner Por Mathias Paulenko

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.

Temas: data

Visión general

El formateo de fechas convierte un Date, datetime o Instant en un string legible; el parseo hace lo contrario. Las dos parecen triviales y las dos fallan en los mismos puntos: los formatos locales ambiguos (02/03/2026 no es el mismo día en EE. UU. y en Europa) y el manejo de zonas horarias.

La regla que ahorra la mayoría de los bugs en producción es simple: guarda y transmite fechas como instantes UTC (ISO 8601 o epoch), y pasa al locale y a la zona del usuario solo en la capa de presentación.

Cuándo usarla

Usa esta receta cuando necesites:

  • Serializar fechas a JSON o XML para una API, donde ISO 8601 es el estándar de facto.
  • Renderizar fechas en una interfaz para que cada lector vea su formato: 10/22/2026 para usuarios de EE. UU., 22/10/2026 para los de España.
  • Analizar fechas introducidas por usuarios o cargadas desde archivos. Consulta Validación de Datos para sanitizar esa entrada.
  • Convertir un instante entre zonas horarias, por ejemplo mostrar la hora de un despliegue en la zona de cada miembro del equipo.
  • Escribir timestamps precisos en logs y registros de auditoría. Consulta Logging para patrones estructurados.

Evita el formateo manual cuando necesites tiempos relativos (“hace 3 horas”) o aritmética de calendario (el próximo día laborable, cada dos martes). Las librerías dedicadas lo resuelven mejor; consulta Variantes.

Solución

Python

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# Instante actual, almacenado en UTC
now_utc = datetime.now(timezone.utc)
print(now_utc.isoformat())  # 2026-06-10T14:30:00+00:00

# Formatear para mostrar
print(now_utc.strftime("%Y-%m-%d %H:%M:%S"))  # 2026-06-10 14:30:00
print(now_utc.strftime("%A, %B %d, %Y"))      # Wednesday, June 10, 2026

# Parsear un string ISO 8601 y convertir de zona
dt = datetime.fromisoformat("2026-06-10T14:30:00+00:00")
madrid = dt.astimezone(ZoneInfo("Europe/Madrid"))
print(madrid.strftime("%Y-%m-%d %H:%M %Z"))  # 2026-06-10 16:30 CEST

# Parsear un formato local de forma explícita
dt2 = datetime.strptime("10/06/2026", "%d/%m/%Y")  # 10 de junio, no 6 de octubre

JavaScript

const now = new Date();

// ISO 8601, siempre UTC
console.log(now.toISOString()); // 2026-06-10T14:30:00.000Z

// Formateo según locale
console.log(now.toLocaleString('en-US', {
  dateStyle: 'full',
  timeStyle: 'short',
  timeZone: 'America/New_York',
})); // Wednesday, June 10, 2026 at 10:30 AM

// Intl.DateTimeFormat, creado una vez y reutilizado
const fmt = new Intl.DateTimeFormat('de-DE', {
  year: 'numeric', month: '2-digit', day: '2-digit',
  hour: '2-digit', minute: '2-digit',
  timeZone: 'Europe/Berlin',
});
console.log(fmt.format(now)); // 10.06.2026, 16:30

// Parsear un string ISO
const parsed = new Date('2026-06-10T14:30:00Z');
console.log(parsed.toISOString());

Java

import java.time.*;
import java.time.format.DateTimeFormatter;

ZonedDateTime nowUtc = ZonedDateTime.now(ZoneOffset.UTC);
System.out.println(nowUtc.format(DateTimeFormatter.ISO_INSTANT));

DateTimeFormatter formatter =
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
System.out.println(nowUtc.format(formatter)); // 2026-06-10 14:30:00 UTC

ZonedDateTime parsed = ZonedDateTime.parse("2026-06-10T14:30:00Z");
ZonedDateTime tokyo = parsed.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println(tokyo.format(formatter)); // 2026-06-10 23:30:00 JST

Cómo funciona

Un instante es un punto fijo en la línea temporal global. La hora local es otra cosa: es lo que muestra un reloj en una zona concreta, y se mueve con el horario de verano. 2026-06-10T14:30:00Z nombra un instante sin ambigüedad; 14:30 a secas puede ser cualquier momento del día.

La otra distinción que confunde es offset frente a zona horaria. +02:00 solo es una distancia fija respecto a UTC, mientras que Europe/Madrid es una zona con nombre en la base de datos IANA, con reglas propias que incluyen el horario de verano: UTC+1 en invierno y UTC+2 en verano. Si congelas el offset en tus datos, vas desfasado una hora durante medio año.

El horario de verano es donde están las trampas. En el cambio de primavera una hora local se salta y 02:30 nunca llega a ocurrir. En el de otoño una hora local ocurre dos veces, así que 01:30 no puede nombrar un solo instante. Trabajar en UTC esquiva ambos problemas.

Flujo de manejo de fechas: parsear entrada, almacenar como ISO 8601 UTC, formatear en la capa de presentación

Chuleta de patrones de formato

Las letras de patrón no son portables entre lenguajes, y las mayúsculas significan cosas distintas en cada uno. En Java, MM es el mes y mm, los minutos; Python usa la misma letra con otra convención: %m para el mes y %M para los minutos. Confundirlas produce salidas incorrectas sin ningún error.

Python strftime / strptime

DirectivaSignificadoEjemplo
%YAño de 4 dígitos2026
%mMes (01-12)06
%dDía del mes (01-31)10
%ANombre completo del día de la semanaWednesday
%BNombre completo del mesJune
%HHora 24h (00-23)14
%IHora 12h (01-12)02
%MMinuto30
%SSegundo00
%pAM o PMPM
%zOffset UTC+0000
%ZNombre de la zona horariaUTC
%jDía del año161

Java DateTimeFormatter

PatrónSignificadoEjemplo
yyyyAño2026
MMNúmero de mes06
MMMNombre corto del mesJun
MMMMNombre completo del mesJune
ddDía del mes10
EEEEDía de la semana completoWednesday
HHHora 0-2314
mmMinuto30
ssSegundo00
XXXOffset ISO 8601Z o +02:00
VVID de zona horaria IANAEurope/Madrid
zNombre corto de la zonaCEST

JavaScript Intl.DateTimeFormat

OpciónValoresSalida de ejemplo
dateStylefull, long, medium, shortWednesday, June 10, 2026
timeStylefull, long, medium, short2:30 PM
yearnumeric, 2-digit2026 o 26
monthnumeric, 2-digit, long, short, narrow6, 06, June, Jun, J
daynumeric, 2-digit10
weekdaylong, short, narrowWednesday, Wed, W
hour, minute, secondnumeric, 2-digit14, 30, 00
hour12true, false2:30 PM vs 14:30
timeZoneID IANAEurope/Madrid
timeZoneNameshort, longCEST

La misma salida en cada lenguaje

SalidaPythonJavaJavaScript
2026-06-10%Y-%m-%dyyyy-MM-dddateStyle: 'short' (depende del locale)
2026-06-10T14:30:00Zisoformat()ISO_INSTANTtoISOString()
Wednesday, June 10, 2026%A, %B %d, %YEEEE, MMMM d, yyyydateStyle: 'full' en en-US
14:30%H:%MHH:mmtimeStyle: 'short' en en-GB
2:30 PM%I:%M %ph:mm ahour12: true en en-US

Variantes y alternativas

  • Python: la stdlib datetime junto a zoneinfo (Python 3.9+) cubre la mayoría de los casos. python-dateutil maneja parseo difuso como “next Friday”. Arrow y Pendulum envuelven el mismo motor con una API más amigable.

  • JavaScript: Intl.DateTimeFormat viene con cada runtime moderno, cuesta 0 KB y conoce todas las zonas IANA. Usa date-fns o Day.js cuando necesites parseo y aritmética, y Luxon cuando la app dependa mucho de las zonas horarias. Evita moment en código nuevo: es mutable, pesa ~70 KB minificado y está en modo mantenimiento.

  • Java: siempre java.time. java.util.Date y SimpleDateFormat son mutables y no son thread-safe.

  • Nota de rendimiento: construir un formateador es la parte costosa. Las instancias de DateTimeFormatter son inmutables y thread-safe, así que almacénalas en caché y reutilízalas. Lo mismo aplica a Intl.DateTimeFormat dentro de un bucle.

  • Companion ejecutable: tres scripts funcionales (Python, JavaScript, Java) en el repo companion con los ejemplos de esta página.

Buenas prácticas

  • Almacena cada fecha como instante UTC y convierte a hora local solo al renderizar para el usuario.
  • Al transmitir por la red, envía siempre ISO 8601 en lugar de un formato propio del locale. Un valor como 2026-06-10T14:30:00Z es el mismo instante para un backend en Python, una API en Node y un servicio en Java.
  • Envía el offset o la zona horaria en las respuestas de API para que el cliente muestre la hora local correcta.
  • Prefiere IDs IANA (America/New_York) frente a abreviaturas (EST), que ignoran el horario de verano y son ambiguas (CST nombra tres zonas distintas).
  • Nunca construyas ni parsees fechas concatenando strings o con expresiones regulares; usa el parser de la plataforma.

Errores comunes

  • Almacenar hora local sin zona: una fila 2026-06-10 14:30 en la base de datos queda irreconstruible en cuanto un servidor o un usuario cambia de zona.
  • Parsear strings no ISO con Date.parse(): cada navegador interpreta 10/06/2026 a su manera; pasa siempre un formato explícito o una librería de parseo.
  • Confundir MM con mm: mes frente a minutos es el bug silencioso más común en los patrones de DateTimeFormatter.
  • Formatear en la zona del servidor: la API contesta en la zona donde corre el servidor y el usuario suele estar en otra. Formatea en la capa de presentación.
  • Asumir que toda hora local existe: una tarea programada a las 02:30 no se ejecuta los días del cambio de primavera en las zonas con horario de verano.

Lectura adicional

Preguntas frecuentes

¿Debería almacenar timestamps o strings formateados en la base de datos?

Guarda instantes en una columna nativa TIMESTAMP WITH TIME ZONE (o segundos epoch), nunca strings formateados. Los strings pierden el instante y ordenan mal entre zonas. Consulta Transacciones de Base de Datos para patrones de integridad relacionados.

¿Cómo manejo el horario de verano?

Mantén todos los valores en UTC internamente y convierte con IDs IANA (Europe/Madrid) al mostrar. Guardar +02:00 en datos o en código se rompe en cuanto cambia el horario de verano.

¿Cuál es la diferencia entre una zona horaria y un offset UTC?

+02:00 es un offset: una distancia fija respecto a UTC que nunca cambia. Europe/Madrid es una zona horaria: una región con nombre cuyo offset salta entre +1 y +2 cuando entra el horario de verano. Almacena instantes y formatea con zonas horarias, no con offsets sueltos.

¿Qué diferencia hay entre toISOString() y toUTCString()?

Las APIs y las bases de datos esperan ISO 8601, y toISOString() produce exactamente eso (2026-06-10T14:30:00.000Z). toUTCString() devuelve el string antiguo RFC 7231 (Wed, 10 Jun 2026 14:30:00 GMT) que aparece en las cabeceras HTTP.

¿Cómo formateo una fecha en el locale del usuario sin una librería?

new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }).format(date) toma el locale del runtime automáticamente. Si el usuario eligió una zona en la configuración, pásala como timeZone explícito.