Dominando la Clase DateTimeFormatter en Java 8

Dominando la Clase DateTimeFormatter en Java 8

1. Overview

En este tutorial, revisaremos la clase DateTimeFormatter de Java 8 y sus patrones de formateo. También discutiremos los posibles casos de uso para esta clase. La clase DateTimeFormatter nos permite formatear fechas y horas de manera uniforme en una aplicación usando patrones predefinidos o definidos por el usuario.

2. DateTimeFormatter Con Instancias Predefinidas

La clase DateTimeFormatter viene con múltiples formatos de fecha/hora predefinidos que siguen estándares ISO y RFC. Por ejemplo, podemos usar la instancia ISO_LOCAL_DATE para analizar una fecha como ‘2018-03-09’:

DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.of(2018, 3, 9));
Para analizar una fecha con un desplazamiento, podemos usar ISO_OFFSET_DATE para obtener un resultado como ‘2018-03-09-03:00’:
DateTimeFormatter.ISO_OFFSET_DATE.format(LocalDate.of(2018, 3, 9).atStartOfDay(ZoneId.of("UTC-3")));
La mayoría de las instancias predefinidas de la clase DateTimeFormatter se enfocan en el estándar ISO-8601, que es un estándar internacional para el formateo de fechas y horas.
Sin embargo, hay una instancia predefinida que analiza RFC-1123, un requisito para los anfitriones de Internet, publicado por la IETF:
DateTimeFormatter.RFC_1123_DATE_TIME.format(LocalDate.of(2018, 3, 9).atStartOfDay(ZoneId.of("UTC-3")));
Este fragmento genera ‘Fri, 9 Mar 2018 00:00:00 -0300.
A veces, debemos manipular la fecha que recibimos como un String en un formato conocido. Para esto, podemos hacer uso del método parse():
LocalDate.from(DateTimeFormatter.ISO_LOCAL_DATE.parse("2018-03-09")).plusDays(3);
El resultado de este fragmento de código es una representación de LocalDate para el 12 de marzo de 2018.
3. DateTimeFormatter Con FormatStyle

A veces, queremos imprimir fechas de manera más legible. En tales casos, podemos usar el enumerado java.time.format.FormatStyle (FULL, LONG, MEDIUM, SHORT) con nuestro DateTimeFormatter:

LocalDate anotherSummerDay = LocalDate.of(2016, 8, 23);
System.out.println(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(anotherSummerDay));
System.out.println(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).format(anotherSummerDay));
System.out.println(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).format(anotherSummerDay));
System.out.println(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(anotherSummerDay));
La salida de estos diferentes estilos de formateo de la misma fecha es:
Tuesday, August 23, 2016
August 23, 2016
Aug 23, 2016
8/23/16
También podemos usar estilos de formato predefinidos para fecha y hora. Para utilizar FormatStyle con tiempo, debemos usar la instancia ZonedDateTime, de lo contrario, se lanzará una DateTimeException:
LocalDate anotherSummerDay = LocalDate.of(2016, 8, 23);
LocalTime anotherTime = LocalTime.of(13, 12, 45);
ZonedDateTime zonedDateTime = ZonedDateTime.of(anotherSummerDay, anotherTime, ZoneId.of("Europe/Helsinki"));
System.out.println(
  DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)
  .format(zonedDateTime));
System.out.println(
  DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)
  .format(zonedDateTime));
System.out.println(
  DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
  .format(zonedDateTime));
System.out.println(
  DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
  .format(zonedDateTime));
La salida que obtenemos es:
Tuesday, August 23, 2016 1:12:45 PM EEST
August 23, 2016 1:12:45 PM EEST
Aug 23, 2016 1:12:45 PM
8/23/16 1:12 PM
También podemos utilizar FormatStyle para analizar un String de fecha y hora, convirtiéndolo en ZonedDateTime, por ejemplo:
ZonedDateTime dateTime = ZonedDateTime.from(
  DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)
    .parse("Tuesday, August 23, 2016 1:12:45 PM EET"));
System.out.println(dateTime.plusHours(9));
La salida de este fragmento es “2016-08-23T22:12:45+03:00[Europe/Bucharest].” Observe que la hora ha cambiado a “22:12:45.”
4. DateTimeFormatter Con Formatos Personalizados

Los formatos predefinidos y los estilos integrados pueden cubrir muchas situaciones. Sin embargo, a veces necesitamos formatear una fecha y una hora de manera diferente. Aquí es donde entran en juego los patrones de formateo personalizados.

4.1. DateTimeFormatter para Fecha

Supongamos que queremos presentar un objeto java.time.LocalDate usando un formato europeo regular como 31.12.2018. Para hacer esto, podríamos llamar al método de fábrica DateTimeFormatter.ofPattern(“dd.MM.yyyy”).

String europeanDatePattern = "dd.MM.yyyy";
DateTimeFormatter europeanDateFormatter = DateTimeFormatter.ofPattern(europeanDatePattern);
System.out.println(europeanDateFormatter.format(LocalDate.of(2016, 7, 31)));
La salida de este fragmento de código será “31.07.2016.”
Hay muchas letras de patrón diferentes que podemos usar para crear un formato para fechas que se ajuste a nuestras necesidades:
 Symbol  Meaning                     Presentation      Examples
 ------  -------                     ------------      -------
   u       year                        year              2004; 04
   y       year-of-era                 year              2004; 04
   M/L     month-of-year               number/text       7; 07; Jul; July; J
   d       day-of-month                number            10
Este es un extracto de la documentación oficial de Java sobre la clase DateTimeFormatter.
El número de letras en el patrón de formato es significativo. Si usamos un patrón de dos letras para el mes, obtendremos una representación de mes de dos dígitos. Si el número del mes es menor que 10, se rellenará con un cero. Cuando no necesitamos ese relleno, podemos usar un patrón de una letra “M”, que mostrará enero como “1.” Si usamos un patrón de cuatro letras para el mes, “MMMM,” obtendremos una representación en “forma completa.” En nuestro ejemplo, sería “July.” Un patrón de cinco letras, “MMMMM,” hará que el formateador use la “forma estrecha.” En este caso, “J” se utilizaría.
Del mismo modo, los patrones de formateo personalizados también se pueden usar para analizar un String que contenga una fecha:
DateTimeFormatter europeanDateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
System.out.println(LocalDate.from(europeanDateFormatter.parse("15.08.2014")).isLeapYear());
Este fragmento de código verifica si la fecha “15.08.2014” es un año bisiesto, que no lo es.
4.2. DateTimeFormatter para Hora

También hay letras de patrón que se pueden usar para patrones de tiempo:

 Symbol  Meaning                     Presentation      Examples
 ------  -------                     ------------      -------
   H       hour-of-day (0-23)          number            0
   m       minute-of-hour              number            30
   s       second-of-minute            number            55
   S       fraction-of-second          fraction          978
   n       nano-of-second              number            987654321
Es bastante simple utilizar DateTimeFormatter para formatear una instancia de java.time.LocalTime. Supongamos que queremos mostrar la hora (horas, minutos y segundos) delimitados con dos puntos:
String timeColonPattern = "HH:mm:ss";
DateTimeFormatter timeColonFormatter = DateTimeFormatter.ofPattern(timeColonPattern);
LocalTime colonTime = LocalTime.of(17, 35, 50);
System.out.println(timeColonFormatter.format(colonTime));
Esto generará la salida “17:35:50.
Si queremos agregar milisegundos a la salida, debemos agregar “SSS” al patrón:
String timeColonPattern = "HH:mm:ss SSS";
DateTimeFormatter timeColonFormatter = DateTimeFormatter.ofPattern(timeColonPattern);
LocalTime colonTime = LocalTime.of(17, 35, 50).plus(329, ChronoUnit.MILLIS);
System.out.println(timeColonFormatter.format(colonTime));
Esto nos da la salida “17:35:50 329.
Observe que “HH” es un patrón de hora del día que genera una salida de 0-23. Cuando deseamos mostrar AM/PM, debemos usar “hh” en minúscula para las horas y agregar un patrón “a”:
String timeColonPattern = "hh:mm:ss a";
DateTimeFormatter timeColonFormatter = DateTimeFormatter.ofPattern(timeColonPattern);
LocalTime colonTime = LocalTime.of(17, 35, 50);
System.out.println(timeColonFormatter.format(colonTime));
La salida generada es “05:35:50 PM.
Podemos también analizar un String de tiempo con nuestro formateador personalizado y verificar si es antes del mediodía:
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("hh:mm:ss a");
System.out.println(LocalTime.from(timeFormatter.parse("12:25:30 AM")).isBefore(LocalTime.NOON));
La salida de este último fragmento muestra que la hora dada está efectivamente antes del mediodía.
4.3. DateTimeFormatter para Zonas Horarias

A menudo queremos ver una zona horaria de alguna variable de fecha-hora específica. Si usamos una fecha-hora basada en Nueva York (UTC -4), podemos usar el patrón de letra “z” para el nombre de la zona horaria:

String newYorkDateTimePattern = "dd.MM.yyyy HH:mm z";
DateTimeFormatter newYorkDateFormatter = DateTimeFormatter.ofPattern(newYorkDateTimePattern);
LocalDateTime summerDay = LocalDateTime.of(2016, 7, 31, 14, 15);
System.out.println(newYorkDateFormatter.format(ZonedDateTime.of(summerDay, ZoneId.of("UTC-4"))));
Esto generará la salida “31.07.2016 14:15 UTC-04:00.
Podemos analizar Strings de fecha-hora con zonas horarias, tal como hicimos anteriormente:
DateTimeFormatter zonedFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm z");
System.out.println(ZonedDateTime.from(zonedFormatter.parse("31.07.2016 14:15 GMT+02:00")).getOffset().getTotalSeconds());
La salida de este código es “7200” segundos, o 2 horas, como se esperaba.
Es fundamental asegurarse de que proporcionemos un String de fecha-hora correcto al método parse(). Si pasamos “31.07.2016 14:15” sin una zona horaria al zonedFormatter del último fragmento de código, obtendremos una DateTimeParseException.
4.4. DateTimeFormatter Usando Locales

No solo es posible usar una zona específica y obtener un horario correcto, sino también producir un formateo de fecha que utilizaría el formato de una localidad específica. Verifiquemos esto con la localidad de EE.UU.:

LocalDate date = LocalDate.of(2023, 9, 18);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMM dd, yy: EEE").withLocale(Locale.US);
String formattedDate = date.format(formatter);
Aquí, deberíamos esperar ver la siguiente salida:
Sep 18, 23: Mon
Probemos usar un formateo más explícito:
LocalDate date = LocalDate.of(2023, 9, 18);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMMM dd, yyyy: EEEE").withLocale(Locale.US);
String formattedDate = date.format(formatter);
Esto produciría el siguiente resultado:
September 18, 2023: Monday
Ahora, podemos cambiar la localidad y producir un formato correctamente formateado para cualquier localidad. En este caso, usaremos Corea. El código sería similar a los fragmentos anteriores y no se incluye por brevedad. Tendríamos la siguiente salida en el primer caso:
9월 18, 23: 월
Y esto para uno más explícito:
9월 18, 2023: 월요일
Esta es una forma conveniente de producir un formateo correcto para cualquier localidad, y Java proporciona esta oportunidad de forma nativa.
4.5. DateTimeFormatter para Instant

DateTimeFormatter viene con un gran formateador ISO instantáneo llamado ISO_INSTANT. Como su nombre implica, este formateador proporciona una forma conveniente de formatear o analizar un instante en UTC.

Según la documentación oficial, un instante no puede ser formateado como una fecha o hora sin especificar una zona horaria. Así que intentar usar ISO_INSTANT en objetos LocalDateTime o LocalDate dará lugar a una excepción:
@Test(expected = UnsupportedTemporalTypeException.class)
public void shouldExpectAnExceptionIfInputIsLocalDateTime() {
    DateTimeFormatter.ISO_INSTANT.format(LocalDateTime.now());
}
Sin embargo, podemos usar ISO_INSTANT para formatear una instancia de ZonedDateTime sin problema:
@Test
public void shouldPrintFormattedZonedDateTime() {
    ZonedDateTime zonedDateTime = ZonedDateTime.of(2021, 02, 15, 0, 0, 0, 0, ZoneId.of("Europe/Paris"));
    String formattedZonedDateTime = DateTimeFormatter.ISO_INSTANT.format(zonedDateTime);
    
    Assert.assertEquals("2021-02-14T23:00:00Z", formattedZonedDateTime);
}
Como podemos ver, creamos nuestro ZonedDateTime con la zona horaria “Europe/Paris”. Sin embargo, el resultado formateado está en UTC.
Del mismo modo, al analizar a ZonedDateTime, necesitamos especificar la zona horaria:
@Test
public void shouldParseZonedDateTime() {
    DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.systemDefault());
    ZonedDateTime zonedDateTime = ZonedDateTime.parse("2021-10-01T05:06:20Z", formatter);
    
    Assert.assertEquals("2021-10-01T05:06:20Z", DateTimeFormatter.ISO_INSTANT.format(zonedDateTime));
}
No hacerlo dará lugar a una DateTimeParseException:
@Test(expected = DateTimeParseException.class)
public void shouldExpectAnExceptionIfTimeZoneIsMissing() {
    ZonedDateTime zonedDateTime = ZonedDateTime.parse("2021-11-01T05:06:20Z", DateTimeFormatter.ISO_INSTANT);
}
También vale la pena mencionar que el análisis requiere especificar al menos el campo de los segundos. De lo contrario, se lanzará una DateTimeParseException.
4.6. Patrones Específicos de Localidad

Java 19 introduce un nuevo método ofLocalizedPattern(String), cuyo nombre podría resultar engañoso, así que vale la pena mencionarlo aquí. El propósito de este método no es proporcionar formateo localizado, como vimos en los ejemplos anteriores con ofPattern(String) combinado con localidad.

El objetivo principal de este método es proporcionar la mejor coincidencia para un patrón proporcionado dado una localidad. La mejor descripción de su intención se puede encontrar en la especificación LDML de Unicode. Utilizan el calendario japonés como ejemplo. En la mayoría de los casos, se debe asociar el año con la era. Mientras que se utiliza la localidad japonesa, todos los patrones que incluyen años se ajustarán al patrón más adecuado para una localidad dada y, por ende, contendrán la era.
Otra cosa que no resulta intuitiva acerca de este método es que lanzará una excepción si no puede coincidir un patrón específico, lo cual puede suceder con frecuencia. El código puede producir errores incluso si los patrones son válidos y funcionarían con el método ofPattern.
5. Conclusión

En este artículo, discutimos cómo usar la clase DateTimeFormatter para formatear fechas y horas. También examinamos ejemplos de patrones que son comunes cuando trabajamos con instancias de fecha-hora.

Para todos los programadores Java, es fundamental dominar DateTimeFormatter para manipular y presentar fechas y horas de manera efectiva en sus aplicaciones. A medida que continúen utilizando esta herramienta, estarán mejor equipados para manejar los sofisticados desafíos del manejo de fechas y horas que enfrentan en el desarrollo ofreciéndoles a sus usuarios la mejor experiencia posible al interactuar con aplicaciones basadas en el tiempo.
Con esto, le animamos a experimentar con distintos patrones y técnicas de formato que ofrece la clase DateTimeFormatter y considerar cómo pueden mejorar sus propias aplicaciones en Java.