23.12.13

Pequeños «gotchas» de Date, Calendar y SimpleDateFormat en Java

El API de tiempo en Java es posiblemente el más vilipendiado, odiado e incomprendido de la plataforma. Pero es lo que hay, y a veces, la única opción que tenemos permitida (no siempre tenemos libertad para elegir librerías), así que voy a escribir un pequeño recetario/recordatorio de aquellas peculiaridades poco intuitivas, que aunque estén perfectamente documentadas, pueden inducir a error si no las tenemos en cuenta.

Primero voy a recordar lo más básico. Para la maquina virtual, la fecha y hora es un long, que indica el número de milisegundos transcurridos desde las 00:00 GMT del 1 de enero de 1970. Punto. No hay más. Las clases Date y Calendar son meros envoltorios con utilidades diversas.

Y ahora sí, vamos con los gotchas.

Meses en Calendar

En la clase Calendar, los meses empiezan desde cero. Es decir, enero es el mes 0, febrero es el mes 1, marzo el 2... y diciembre es el mes 11. La propia clase nos ofrece unas constantes con los nombres de los meses en inglés (JANUARY, FEBRUARY), pero es fácil olvidar este detalle y tener un disgusto. Sobre todo, cuando la clase SimpleDateFormat sí sigue el criterio intuitivo, y los meses empiezan con uno (enero es 1, febrero es 2, diciembre es 12).

HOUR vs. HOUR_OF_DAY

Calendar nos ofrece muchas constantes para referirnos a las distintas partes de la fecha y hora, pero cuidado con ellas. La constante HOUR se refiere a la hora, pero exclusivamente en formato AM/PM. Eso quiere decir que su valor está comprendido entre 1 y 12. Si hacemos


    calendar.set(Calendar.HOUR, 6);

estaremos estableciendo un valor diferente dependiendo de en qué momento del día se ejecute. Si es antes de las 12:00, estaremos indicando que la hora es 6 (cosa que seguramente es lo que queremos hacer), pero si ese código se ejecuta a las 12:00 o después, estaremos estableciendo 18 como hora. Si queremos usar el formato 24H (y especificar como hora un valor entre 0 y 23) debemos usar la constante HOUR_OF_DAY. Por ejemplo:


    calendar.set(Calendar.HOUR_OF_DAY, 6);

DATE no es lo que parece

Otra constante engañosa: DATE es equivalente a DAY_OF_MONTH y representa el día del mes. Haríamos bien en no usarla nunca, pero podríamos encontrarla en código ajeno, y conviene recordar qué significa.

Métodos iguales que no hacen lo mismo

Tanto Date como Calendar tienen un método getTime(), pero ojo, porque devuelven cosas diferentes. El getTime() de Date devuelve un long con el tiempo interno (los famosos milisegundos transcurridos desde las 00:00 GMT del 1 de enero de 1970), mientras que el getTime() de Calendar devuelve un objeto Date. Si queremos el long con los milisegundos, debemos usar getTimeInMillis()

Horas en SimpleDateFormat

Vamos con SimpleDateFormat y la forma de especificar un patrón. La letra «h» (minúscula) indica la hora en formato AM/PM, mientras que la «H» la indica en formato 24H. El siguiente formateador:


    SimpleDateFormat sdf = new SimpleDateFormat("hh:mm");

Nos devolverá 6:30, tanto si le pasamos un Date con la hora establecida a las 6:30 como a las 18:30. Si queremos usar el formato 24H (más habitual por estos lares), debemos usar:


    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");

SimpleDateFormat tiene otras letras para especificar la hora, pero creo que podemos ignorarlas, ya que nunca las he usado, y no creo que alguien lo haga por accidente («k» para 1-24 y «K» para 0-11).

No hay validaciones

Un comportamiento curioso de Calendar y SimpleDateFormat es que no hay limitación en el rango de valores. Es decir, podemos indicar fechas como el 31 de febrero, o el 40 de mayo. Como internamente la fecha en el fondo es un long con los milisegundos transcurridos desde la referencia 0, el valor incorrecto se convierte automáticamente en uno correcto. Así, el 31 de febrero correspondería al 3 de marzo (o el 2, si es un año bisiesto), y el 40 de mayo al 9 de junio (fecha hasta la que no debemos quitarnos el sayo). Eso quiere decir que no podemos usar estas clases para realizar algún tipo de validación en los rangos de valores de una entrada (como un formulario web, por ejemplo).

XML dateTime

Termino con algo que no es un «gotcha» sino una limitación. No hay forma de especificar un patrón para SimpleDateFormat que cumpla con el estándar XML, si queremos especificar la zona horaria. Si no especificamos la zona, basta con el siguiente patrón:


    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

o incluso


    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");

si queremos llegar hasta el milisegundo. Pero si necesitamos especificar la zona horaria, tenemos un problema. SimpleDateFormat nos ofrece dos formas de pintar o parsear la zona horaria: «z», que es una representación textual larga y no nos sirve, y «Z» que es más corta y casi nos sirve. El «casi» es porque SimpleDateFormat representa la zona horaria con como la diferencia con GMT (Greenwich) en formato «+/-HHmm», es decir, el horario peninsular de invierno es «+0100». Pero en el estándar XML, las horas y minutos de diferencia están separados por dos puntos («:»), de forma que el ejemplo anterior se representaría como «+01:00».

Si la zona horaria va a ser siempre GMT, podemos aprovecharnos de que en el estándar XML, dicha zona horaria se representa como «Z», y hacer lo siguiente:


    public String toXmlString(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
        return sdf.format(date);
    }

Pero si debemos usar otras zonas, y no podemos usar alguna librería más adecuada (el no uso de determinadas librerías puede ser un dato del problema), no nos queda más remedio que implementar algo más manual.