W3docs

Análisis de fechas en Java

Convierte cadenas en objetos de fecha y hora de Java con DateTimeFormatter y gestiona las excepciones de análisis.

El análisis es el espejo del formateo. El mismo DateTimeFormatter, el mismo alfabeto de patrones, las mismas advertencias — pero leyendo una cadena en lugar de escribir una. Cada tipo de java.time tiene una fábrica parse(...); con un patrón predeterminado (ISO-8601) acepta un argumento y, con un patrón personalizado, acepta un formateador.

LocalDate    d  = LocalDate.parse("2025-11-04");                                       // ISO default
LocalTime    t  = LocalTime.parse("14:30:00");
LocalDateTime dt = LocalDateTime.parse("2025-11-04T14:30:00");                          // note the T
ZonedDateTime zdt = ZonedDateTime.parse("2025-11-04T14:30:00-05:00[America/New_York]");
Instant       i = Instant.parse("2025-11-04T19:30:00Z");                                // trailing Z mandatory

Cada uno de estos usa el formateador predeterminado del tipo — ISO-8601 estricto. Para cualquier cosa que no tenga forma ISO, construye un formateador y pásalo.

Análisis con patrón personalizado

DateTimeFormatter f = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate d = LocalDate.parse("04/11/2025", f);                                         // British DMY

El alfabeto de patrones es el mismo que para el formateo — dd MMM yyyy, HH:mm:ss, MM/dd/yyyy h:mm a. La regla de coincidencia es estricta por defecto: cada carácter literal del patrón debe aparecer exactamente en la entrada, y cada campo debe tener el número correcto de dígitos.

DateTimeFormatter dmy = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate.parse("4/11/2025", dmy);                                                       // FAILS: "dd" requires 2 digits
LocalDate.parse("04/11/2025", dmy);                                                      // OK

Si la entrada a veces tiene un solo dígito, usa d/M/yyyy (que acepta 1-2 dígitos) o construye el formateador con DateTimeFormatterBuilder y parseStrict(false). La forma de una sola letra es la solución más sencilla.

La configuración regional importa en el límite del análisis

La misma historia de configuración regional que para el formateo: los nombres de meses (MMMM) y los nombres de días de la semana (EEEE) son específicos del idioma, por lo que el formateador debe saber en qué idioma está escrita la entrada.

DateTimeFormatter englishDay = DateTimeFormatter.ofPattern("EEEE, MMMM d yyyy", Locale.US);
DateTimeFormatter germanDay  = DateTimeFormatter.ofPattern("EEEE, d. MMMM yyyy",  Locale.GERMAN);

LocalDate.parse("Tuesday, November 4 2025", englishDay);   // 2025-11-04
LocalDate.parse("Dienstag, 4. November 2025", germanDay);  // 2025-11-04 (German month name)

Observa el yyyy en ambos patrones. Para producir un LocalDate, la entrada debe proporcionar un año — un patrón como "EEEE, MMMM d" se analiza, pero solo devuelve un TemporalAccessor sin campo de año, por lo que LocalDate.parse lanzará una excepción. Si tus cadenas genuinamente no tienen año, analiza a un TemporalAccessor y combínalo con un año tú mismo.

Sin una configuración regional explícita, se usa Locale.getDefault() — y la configuración regional predeterminada de la JVM de un servidor es impredecible. Siempre pasa una configuración regional al analizar nombres de meses o días de una cadena que el usuario podría escribir. El espejo de "siempre formatear con una configuración regional" se aplica aquí.

DateTimeParseException

Un análisis fallido lanza DateTimeParseException (una subclase de RuntimeException, por lo que no se declara en parse). El mensaje indica tanto la posición como lo que se esperaba:

try {
  LocalDate.parse("2025-13-45");                              // month 13, day 45
} catch (DateTimeParseException e) {
  e.getParsedString();                                         // "2025-13-45"
  e.getErrorIndex();                                           // index where parsing gave up
  e.getMessage();                                              // human description
}

Dos tipos distintos de fallos llegan aquí:

  • Desajuste de formato. La cadena no encaja en absoluto con el patrón — "04 nov 2025" con "dd-MM-yyyy".
  • Valor fuera de rango. La cadena encaja con el patrón, pero un valor es imposible — mes 13, día 32.

Ambos lanzan la misma clase. Captura e informa; nunca los ignores silenciosamente.

Los años de dos dígitos y la trampa de MAX_VALUE

El patrón yy (año de dos dígitos) tiene un comportamiento predeterminado documentado pero sorprendente: se analiza como el año más cercano al día de hoy dentro de una ventana de 100 años. LocalDate.parse("11/04/25", DateTimeFormatter.ofPattern("MM/dd/yy")) da 2025-11-04 en 2025 y 2125-11-04 en 2076. Eso es una característica para casos "cerca de hoy" y un peligro para datos de archivo.

La solución es usar yyyy siempre que la entrada tenga cuatro dígitos y ser explícito sobre la ventana de siglo cuando no los tenga:

DateTimeFormatter f = new DateTimeFormatterBuilder()
    .appendPattern("MM/dd/")
    .appendValueReduced(ChronoField.YEAR, 2, 2, 1950)         // window starts at 1950
    .toFormatter();

Si estás manejando datos heredados con yy, documenta la ventana en el código. El valor predeterminado es "pivote rodante alrededor del año actual", que no es lo que quieres para "todos mis datos son de los años 80".

Análisis sin comprometerse con un tipo

DateTimeFormatter.parse(String) devuelve un TemporalAccessor — el fondo de la jerarquía de tipos. Útil cuando la entrada puede ser un LocalDate o un LocalDateTime:

TemporalAccessor ta = DateTimeFormatter.ISO_DATE_TIME.parseBest(
    "2025-11-04T14:30:00",
    LocalDateTime::from,                                       // preferred
    LocalDate::from);                                          // fallback

parseBest(text, ...queries) prueba el método from de cada tipo en orden y devuelve el primero que tiene éxito. El resultado necesita instanceof para usarlo para algo específico:

if (ta instanceof LocalDateTime ldt) ...
else if (ta instanceof LocalDate ld) ...

Para la mayoría del código, llamar directamente al parse(...) del tipo específico es más sencillo. parseBest es para el caso en que aceptas múltiples formas (una columna CSV que podría ser una fecha o una fecha-hora).

Análisis flexible con el constructor

DateTimeFormatterBuilder te permite ensamblar un formateador más indulgente:

DateTimeFormatter lenient = new DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd[ HH:mm[:ss]]")                  // optional sections in []
    .parseLenient()                                            // accept missing leading zeros etc.
    .parseCaseInsensitive()                                    // ignore case on month/day names
    .toFormatter(Locale.US);

La sintaxis de corchetes [...] marca una sección opcional — ese patrón analiza tanto "2025-11-04" (sin hora) como "2025-11-04 14:30" (con hora). Combinado con parseLenient y parseCaseInsensitive, puedes construir un formateador que acepte una gama más amplia de entradas sin escribir un analizador personalizado.

Esto es exagerado para código que controla ambos extremos. Usa el valor predeterminado estricto a menos que estés leyendo entradas de usuario o datos heredados.

Instant.parse es estricto

Instant.parse("2025-11-04T19:30:00Z") funciona. La Z final (UTC) es obligatoria; cualquier otro desplazamiento (-05:00, +09:00) necesita primero OffsetDateTime.parse o ZonedDateTime.parse, y luego .toInstant():

Instant inst = OffsetDateTime.parse("2025-11-04T14:30:00-05:00").toInstant();

Esta es la conversión canónica cuando una API externa te entrega cadenas ISO-8601 con desplazamientos de zona horaria pero sin zona IANA.

Un ejemplo práctico: leer una configuración, analizar y reaccionar ante entradas incorrectas

El programa a continuación analiza tres cadenas con forma de fecha de una configuración sintética: una fecha ISO, una fecha con patrón personalizado y un instante ISO. Luego demuestra el constructor flexible con una sección de tiempo opcional, la API parseBest para "ya sea una fecha o una fecha-hora", y el modo de fallo cuando la entrada no coincide.

java— editable, runs on the server

Qué extraer de la ejecución:

  • Los tres analizadores ISO predeterminados aceptaron exactamente la forma estándar: yyyy-MM-dd para LocalDate, la forma separada por T para LocalDateTime y la forma terminada en Z para Instant. Sin flexibilidad, sin suposiciones — y ese es el objetivo. Si tu entrada encaja, no se necesita formateador; si no encaja, construir uno es una sola línea.
  • El formateador flexible aceptó tres formas de entrada diferentes — solo fecha, fecha con hora, fecha con hora y segundos — porque la sección entre corchetes [...] es opcional. parseBest(text, LocalDateTime::from, LocalDate::from) eligió el tipo más rico que cada entrada soportaba. Ese es el patrón correcto cuando aceptas fechas de configuración o ingresadas por el usuario con precisión variable.
  • OffsetDateTime.parse(wire).toInstant() fue el puente canónico de "una marca de tiempo ISO-8601 con un desplazamiento" a Instant. El propio Instant.parse solo acepta UTC terminado en Z; cualquier otra cosa debe pasar primero por OffsetDateTime (o ZonedDateTime). La conversión es una línea en cada dirección.
  • El análisis con configuración regional alemana solo funcionó porque el formateador se construyó con Locale.GERMAN. La configuración regional predeterminada habría rechazado "November" si la JVM se estuviera ejecutando en alemán (que espera que "November" coincida con los nombres en alemán de Locale.GERMAN). Siempre fija la configuración regional en el límite del análisis — el formateador por sí solo no es suficiente; la configuración regional controla la resolución de nombres de meses y días.
  • Los dos bloques de fallo lanzaron DateTimeParseException con información útil sobre la posición. getErrorIndex indica dónde se detuvo el analizador; getParsedString es la entrada tal como la vio el analizador. Muestra esto en los errores orientados al usuario — "no se pudo analizar la fecha en el carácter 5" es mucho más útil que "fecha inválida".

Qué sigue

El formateo y el análisis cierran el límite de cadenas. El siguiente capítulo, Java Temporal Adjusters, vuelve al lado de los valores y cubre los ajustadores integrados (firstDayOfMonth, nextOrSame(MONDAY), etc.) y cómo escribir los tuyos propios — útil siempre que la fecha que quieres dependa de la fecha que tienes ("primer martes después del día 15").

Práctica

Práctica
Un formulario web permite a los usuarios escribir una fecha en el formato '04/11/2025' (día/mes/año). Tu código usa `LocalDate.parse(input, DateTimeFormatter.ofPattern('dd/MM/yyyy'))` y luego pregunta '¿esta fecha está en el futuro?'. Un usuario escribe '4/11/2025' (sin cero inicial en el día) y el análisis lanza una excepción. ¿Cuál es la solución más pequeña?
Un formulario web permite a los usuarios escribir una fecha en el formato '04/11/2025' (día/mes/año). Tu código usa `LocalDate.parse(input, DateTimeFormatter.ofPattern('dd/MM/yyyy'))` y luego pregunta '¿esta fecha está en el futuro?'. Un usuario escribe '4/11/2025' (sin cero inicial en el día) y el análisis lanza una excepción. ¿Cuál es la solución más pequeña?
Was this page helpful?