La mejor manera de resolver problemas de conversiones de horas entre diferentes husos horarios en Java es utilizando la API de Joda Time. Esta librería ofrece una poderosa API de manipulación y procesamiento de fechas, horas y demás.
Existen varios problemas a la hora de aplicar conversiones de este tipo, principalmente:
- Los objetos tipo Date o Timestamp no contienen información sobre husos horarios, por lo que toman por defecto el que establece la JVM en la que se están ejecutando.
- Existen ciertas horas, como por ejemplo las horas UTC que coinciden con cambios de hora de invierno a verano, que no existen en la mayoría de los husos horarios, por lo que los métodos clásicos de conversion de hora en Java suelen fallar al convertir estas horas.
Es decir, que conversiones de horas como las 02:00 UTC a cualquier huso horario que aplica el cambio de horario de invierno a verano y viceversa (DST en inglés), suelen fallar si se realizan las conversiones a partir de objetos tipo Date o Timestamp o se usa la clase SimpleDateFormat para realizar el cambio, ya que esa hora no existe en el huso horario de destino. Por ello se recomienda la API de Joda Time, que maneja bien todos los posibles casos de conversion entre horas.
Por lo tanto, los criterios seguidos en la implementación de las funciones que se presentan posteriormente para realizar conversiones de tipo UTC a hora local o viceversa, asegurándonos que funcionan correctamente en cualquier caso, son los siguientes:
- Pasar la fecha y hora original como texto. Asi evitamos cualquier problema con aquellas horas que no existen en la zona horaria local.
- Indicar siempre la zona horaria a la que se quiere convertir, o desde la que se va a convertir.
1. De UTC a local (o a una zona horaria específica)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public static Date findLocalTime(String dateUTCStrArg, String format, TimeZone localZone) throws ParseException { /* Set format */ if (format == null || "".equals(format)) { /* Default format */ format = SDFFORMAT; } /* Get UTC datetime */ DateTimeFormatter fmt = DateTimeFormat.forPattern(format); DateTime dtUtc = fmt.withZone(DateTimeZone.UTC).parseDateTime(dateUTCStrArg); /* DateTime Local time */ DateTime dtLocal = dtUtc.withZone(DateTimeZone.forID(localZone.getID())); return dtLocal.toLocalDateTime().toDateTime().toDate(); } |
En la función, primero se parsea la fecha como UTC, ya que existen horas en UTC como se ha mencionado antes, que no existen en otros husos horarios. Una vez se tiene el DateTime en UTC, se obtiene su equivalente en la zona horaria a convertir usando la API de Joda y a partir de ahi se puede obtener un Date ya que cualquier hora “local” generada debería existir en nuestra zona horaria. Una alternativa a retornar un Date puede ser devolver un Calendar o el propio DateTime generado, que dispondrían de la información de la zona horaria, digamos si posteriormente se quieren hacer manipulaciones con la hora y fecha obtenidas.
2. De local a UTC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static Calendar findUTCCalendar(String dateStrArg, String format, TimeZone localZone) throws ParseException { /* Set format */ if (format == null || "".equals(format)) { format = SDFFORMAT; } /* Get local datetime */ DateTimeFormatter fmt = DateTimeFormat.forPattern(format); DateTime dtLocal = fmt.withZone(DateTimeZone.forID(localZone.getID())).parseDateTime(dateStrArg); /* DateTime utc time */ DateTime dtUtc = dtLocal.withZone(DateTimeZone.UTC); return dtUtc.toGregorianCalendar(); } |
El caso de la conversión de local a UTC es similar al anterior, con la salvedad de que como se pueden generar horas de UTC que no existen en otros husos horarios, no se puede retornar un Date. Hay que devolver un objeto que contenga información sobre el huso horario, como un Calendarm por ejemplo. Así nos aseguramos de que en cualquier caso, la función será capaz de realizar la conversión de forma correcta.
3. Función genérica de conversión
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static Calendar findCalendarForZone(String dateStr, String format, String srcTz, String destTz) { if (format == null || "".equals(format)) { format = SDFFORMAT; } DateTimeFormatter fmt = DateTimeFormat.forPattern(format); DateTime srcDateTime = fmt.withZone(DateTimeZone.forID(srcTz)).parseDateTime(dateStr); DateTime dstDateTime = srcDateTime.withZone(DateTimeZone.forID(destTz)); try { return dstDateTime.toLocalDateTime().toDateTime().toGregorianCalendar(); } catch (IllegalArgumentException iae) { return dstDateTime.toGregorianCalendar(); } } |
La anterior sería una versión de una función genérica de conversión, que podría realizar ámbas operaciones, retornando un Calendar en caso de que la hora a devolver no exista en el huso horario establecido en la JVM.