lunes, marzo 04, 2013

Java y la gestión de fecha - hora

Siguiendo con los  artículos referidos a las obviedades olvidadas, y cuyo primer artículo técnico fue este, ahora nos adentramos en la gestión de fechas y horas.

En muchísimas aplicaciones se manejan fechas y horas, pero no por ello les prestamos la atención que se merecen. ¿La hora que manejamos es siempre la hora local? ¿Y qué pasa cuando nos comunicamos con un servicio en otro lugar del mundo? ¿Y cómo la guardo en base de datos? ¿se mantiene la zona horaria? ¿Cómo sabemos si hoy es domingo?

Fechas

Mas que de fechas, deberiamos hablar de calendarios. No solo existe el calendario europeo o Gregoriano, sino que también hay años según los árabes, o según los japoneses. La semana puede empezar el domingo o el lunes. El cambio de hora por motivos de luz solar se puede hacer un día u otro. Las zonas horarias no tienen porque ser siempre diferencias de horas enteras respecto de UTC,...
El formato de notación internacional estandar es AAAAMMDD o AAAA-MM-DD, indicado en ISO 8601.
Si le añadimos la hora, sería hhmmss o hh:mm:ss
Si queremos añadir la zona horaria, sería Z (para UTC) o +/-HHMM para indicar la diferencia.
Si concatenáramos todo, sería 2013-02-26T13:15:15+0200
Tiene como ventajas:
  • fácil de escribir y leer por software
  • fácil de comparar y ordenar
  • independiente del lenguaje
  • longitud constante

API estandar

Java, desde su primera versión ya incluía una serie de clases para gestionar fechas y horas. En sucesivas versiones han ido añadiendo más y más métodos.

Las clases principales son:
  • java.util.Date : No es más que un encapsulador de los milisegundos desde el 1 de enero de 1970, a las 12:00 UTC  sin zona horaria
  • java.util.Time . La hora sin zona horaria
  • java.util.Timestamp . Fecha y hora
  • Calendar : para manipular fechas. Aunque en realidad acabas usando alguna clase hija como GregorianCalendar. Si admite gestión de la zona horaria.
  • java.text.SimpleDateFormat permite convertir de / a nuestras fechas y horas a / de texto.
Estas clases no están preparadas para el trabajo en un entorno multitarea, por lo que, por ejemplo, si usáramos la misma instancia de SimpleDateFormat para leer o formatear alguna fecha, en varios hilos, podría darnos resultados inesperados y erróneos.
Cuando trabajamos con fechas-horas hay 3 problemas que son los fundamentales:
  1. manejo correcto de las horas los dias de cambio de horario
  2. años bisiestos
  3.  manejo de horas en diferentes zonas horarias
Respecto de los dias con cambio horario, se recomienda trabajar SIEMPRE a nivel interno de la hora UTC, y sólo convertir de / a local cuando sea necesario, de esa manera no nos afectarian los cambios horarios (ejemplo típico es la hora en TDT, que viaja en UTC, y también viaja con una señal indicando la zona horaria, para que la empleen los descodificadores de TDT).

Zonas horarias

Con el siguiente código podemos observar algunas de las características del manejo de las zonas horarias:
import java.util.*;
import java.text.*;

public class convertir_timezone
{
  public static String formateaCalendar(String formato, Calendar cal)
  {
    SimpleDateFormat formateador = new SimpleDateFormat(formato);
    formateador.setCalendar(cal);
    return formateador.format(cal.getTime());
  }
  public Calendar convierteAZonaHoraria(Calendar cal, TimeZone tz)
  {
    System.out.println("Hora original:" + new Date(cal.getTimeInMillis()));

    //Obtenemos las diferencias horarias en ambos casos
    int LocalOffSethrs = (int) ((cal.getTimeZone().getRawOffset()) *
      (2.77777778 /10000000));        
    int dstOffsethrs = (int) ((tz.getRawOffset()) *
      (2.77777778 /10000000));       
    //Diferencias por horario de verano / invierno
    int dts = cal.getTimeZone().getDSTSavings();
    System.out.println("Zona horaria original : " +
      cal.getTimeZone().getDisplayName());
    System.out.println("Diferencia por horario verano / invierno: " +
      dts);
    System.out.println("Diferencia en la zona "+tz.getID()+" : " +
      tz.getRawOffset());
    System.out.println("Diferencia horaria de zona original: " +
      LocalOffSethrs);
    System.out.println("Diferencia horaria de zona "+tz.getID()+": " +
      dstOffsethrs);    
    Calendar calDst=(Calendar)cal.clone();//Copiar la fecha
    // Ajustar a UTC
    //calDst.add(Calendar.MILLISECOND,-(cal.getTimeZone().getRawOffset()));  
    // Ajustar la diferencia de verano / invierno
    //calDst.add(Calendar.MILLISECOND, - cal.getTimeZone().getDSTSavings());
    // Ajustar la diferencia
    //calDst.add(Calendar.MILLISECOND, tz.getRawOffset());       
    calDst.setTimeZone(tz);
    Date fecha = new Date(calDst.getTimeInMillis());              
    System.out.println("Hora tras ajustar la diferencia horaria:" + fecha);
    System.out.println("Calendar tras ajustar: "+
      formateaCalendar("yyyy-MM-dd'T'HH:mm:ss.SSSZ", calDst));
    return calDst;
  }
  public static void main(String []args)
  {
    convertir_timezone c=new convertir_timezone();
    Calendar cal=Calendar.getInstance();//Ahora
    System.out.println("Calendar original: "+
      c.formateaCalendar("yyyy-MM-dd'T'HH:mm:ss.SSSZ", cal));
    //TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
    TimeZone tz = TimeZone.getTimeZone("America/Los_Angeles");
    Calendar calDst=c.convierteAZonaHoraria(cal, tz);
  }
}

Como podemos observar, para cambiar de zona horaria, tenemos 2 opciones:
  1. Trabajar solo con objetos Calendar y, tras tener uno bien definido, le cambiamos la zona horaria.
  2. Andar jugando con  las diferencias de tiempo respecto de UTC (indicado en los comentarios)

También conviene indicar que la JVM viene con una pequeña base de datos de zonas horarias y, en general, suele conocer unas cuantas, por nombre genérico y por nombre bonito (como en el ejemplo que hemos indicado Los_Angeles).

Diferentes calendarios

Hasta Java 1.6 se soportan 3 calendarios distintos: Gregoriano, Budista y Japonés.

//No funciona con java.util.Local.JAPANESE
locale=java.util.Local("ja", "JP", "JP");
//Conseguimos un Calendar de ahora, con la fecha como Japón
Calendar cal=java.util.Calendar.getInstance(l);
//Calendar con calendario Gregoriano
Calendar cal2=java.util.Calendar.getInstance();

//Convertimos la fecha japonesa en fecha Gregoriana
Date fecha=cal.getTime();
cal2.setTime(fecha);

Por si alguien tiene curiosidad, la diferencia entre el calendario Japonés y el Gregoriano es que en el Japonés hay un nuevo campo, la ERA, que indica la monarquía que nos marca la referencia del año, con lo que también tendrán diferente el año.

Día de la semana

Para conocer el día de la semana, se puede preguntar a Calendar pero, ¿y si queremos saber si es Viernes o Domingo? Los índices de la semana podrían variar de USA, donde comienzan en Domingo, a España, que se comienza en Lunes.

Calendar ahora=java.util.Calendar.getInstance();
//Para saber el índice del día que es Viernes
int hoy=now.FRIDAY; //Aqui devolvería 6
//Si nos devuelve 6, seria Viernes
ahora.get(Calendar.DAY_OF_WEEK);

Hay que preguntar al propio Calendario para saber el índice del día que se corresponde con el día de la semana que nos interesa.

API nuevo

En el futuro JDK 8 vendrá incluida la nueva implementación de API de fechas y horas, correspondiente al JSR 310.
Toda la biblioteca colgará del paquete java.time, y estará compuesta por clases inmutables y compatibles con threads.
Las clases más importantes son:
  • LocalDate : fecha sin hora ni zona horaria.
  • LocalTime: hora sin fecha ni zona horaria.
  • LocalDateTime : hora y fecha sin zona horaria
  • ZonedDateTime: fecha y hora con zona horaria.
  • Instant : Momento en el tiempo con resolución de nanosegundos. Sería el equivalente antiguo a hacer new java.util.Date()
  • Clock: momento en el tiempo, pero con zona horaria.
  • ZoneId y ZoneOffset : id y diferencia horaria de una zona horaria

API alternativo -> Joda-time

Joda-time es una API alternativa a Date, Time,... que vienen con java.
En general, se considera que la API estandar es poco intuitiva y muy dada a llevarnos a cometer errores. Ya sea por lo largo que es crear una fecha u hora exactas facilmente, o conseguir la zona horaria por nombre, jerarquía de clases confusa (tanto java.sql.Date como java.sql.Time extienden java.util.Date), ¿por qué comienzan a contar los meses desde el 0?...
Joda-time es una API que facilita y prepara para la transición a la nueva API de fechas.
Características generales:
  • La mayoría de sus clases son inmutables, evitando así problemas de sincronía con hilos
  • las clases tienen formas sencillas de convertir de / a clases de la API estandar
  • Sustituyen el concepto de Calendar a uno de Chronology , mas general, ya que añade formas de aritmética "diferentes" para cada caso.
  • Se ven estructuras del tipo SystemFactory.getClock().getLocalDate().plusDays(6).dayOfWeek()
Sus ventajas:
  • Contiene muchos métodos fáciles de leer como plusDays
  • Ya de fábrica viene con 8 sistemas de calendario
  • Afirman que tiene un mayor rendimiento
Desventajas:
  • Problemas de integración con frameworks que no lo esperan, aunque esta desventaja es cada vez menor, ya que poco a poco lo van soportando, como Sping MVC 3 . Para otras API, cono Hibernate, van haciendo los wrappers o similar, necesarios, pero no siguen al mismo ritmo que Hibernate.
  • Es una nueva dependencia que añadir a nuestro proyecto, ficheros de distribución,...

Fechas en JDBC

 Lo primero que llama la atención es que en JDBC se emplea java.sql.date, en lugar de las clases de java.util.*. Pueden surgir multitud de problemas en el manejo y almacenamiento de fecha-hora en base de datos porque dependiendo del gestor de Base de Datos y su localización, de la configuración del driver y la localización de la JVM,... los resultados pueden ser inesperados.
A efectos prácticos, en la mayoría de los casos podemos suponer que la fecha - hora que se almacena en el servidor es sin zona horaria, y será la aplicación / cliente quien sepa en que fue introducido aquello.
Con el tiempo van surgiendo soluciones. Por ejemplo, en DB2 10 ya hay un nuevo tipo de datos que es "TIMESTAMP WITH TIME ZONE" que permite almacenar también la zona horaria.
Las clase más importantes son:
  • java.sql.Date, se corresponde con SQL DATE
  • java.sql.Time, se corresponde con SQL TIME
  • java.sql.Timestamp , se corresponde con SQL TIMESTAMP aunque, en teoría, en la base de datos puede ser nanosegundos, y en Java nos quedamos con milisegundos
Para pasar de java.util.Date a java.sql.Date:  
java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());

O
java.sql.Timestamp sqlTimestamp = new java.sql.Timestamp(utilDate.getTime());
En el sentido contrario es más sencillo ya que java.sql.Date extiendo java.util.Date:
java.util.date newDate = result.getDate("Columna")

Yo reconozco que, en muchas ocasiones, he guardado timestamps como VARCHAR en base de datos relacionales para evitar estos problemas.
Como mejora, se recomienda emplear joda-time con el plug para JDBC, que también tiene amplizaciones para que funcione facilmente con Hibernate.

Fecha - hora en log4j

También es importante configurar el patrón de salida de log4j para conocer realmente cuando ocurrió un evento. He visto servidores con configuraciones que hacían muy difícil saber la hora a la que pasó algo, y más por culpa de no saber en que zona horaria están.
Si no ponemos nada, supondrá formato ISO 8601.
Para indicar el formato de la fecha-hora en la salida de log4j tenemos 2 opciones:
  1. Dentro de un org.apache.log4j.PatternLayout indicar el formato de la salida, como si fuera un SimpleDateFormat. Ej: %d{dd MMM yyyy HH:mm:ss,SSS} . Esta es la opción última, ya que se considera LENTA.
  2. Emplear uno de los formateadores que ya vienen con log4j. Hay 3:
    1. AbsoluteTimeDateFormat ( "HH:mm:ss,SSS" )
    2. DateTimeDateFormat ( "dd MMM yyyy HH:mm:ss,SSS" )
    3. ISO8601DateFormat ( "yyyy-MM-dd HH:mm:ss,SSS" )
Para que use estos formateadores, habría que poner en el patrón %d{ABSOLUTE}, %d{DATE} o %d{ABSOLUTE}

No hay comentarios: