Manejo De Fechas En Java I

Por Carlos Zuluaga (Mayo 1 de 2007).

Trabajar con fechas en java no es algo del otro mundo ni tiene demasiadas complicaciones, pero la cantidad de formas que hay para hacerlo puede confundirnos, o peor aún, puede que sólo conozcamos la más mala para hacerlo. Con estos artículos pretendo explicar un poco que clases tiene el lenguaje para trabajar con fechas, los métodos más usados y algunas sugerencias para realizar un trabajo adecuado que nos facilite el mantenimiento de nuestras aplicaciones.

1. Las clases java.util.Date y java.sql.Date. Son dos de las clases más usadas cuando una aplicación implica el trabajo con fechas:
java.util.Date: Según la documentación "La clase java.util.Date representa un instante de tiempo específico, con precisión de milisegundos"; esto más que ser una especie de "autoadulación" para la clase, quiere decir que no solo se trata de una simple cadena al estilo yyyy/MM/dd, sino que almacena hasta milisegundos y que es posible trabajar con ellos.
Antes del jdk1.1 la clase java.util.Date tenía dos funciones adicionales a la que conocemos ahora, una de ellas era la interpretación de datos que tenían que ver con fechas, como años, días, segundos, entre otros. La otra era el formateo (la forma como se muestra) y parseo (convertir un String a java.util.Date). Pero debido a las dificultades que presentaban estas funcionalidades a la hora de internacionalizar los programas, esos métodos ya está obsoletos y la clase java.util.Calendar se encargó de esto; así que en este momento esta clase, sólo hace lo que se mencionó al principio: "representa un instante de tiempo específico, con precisión de milisegundos"; más adelante veremos como ampliar esta funcionalidad. Por ahora veamos las convenciones que sigue esta clase:
* El año "y" está representado por un entero igual a ("y" - 1900). Por ejemplo el año 2004 se representa como 104 (2004 - 1900).
* Los meses son representados por números entre 0 y 11, donde enero es 0 y diciembre es 11.
* Los días y minutos se representan de forma corriente. Entre 1 - 31 y 0 - 59 respectivamente.
* Las horas van entre 0 y 23, donde la medianoche es 0 y el medio día 12.
* Los segundos van entre 0 y 61. 61 solo ocurre cuando se agrega el segundo adicional para ajustar la diferencia entre el reloj atómico y el tiempo de rotación de la tierra.
No sobra mencionar que los métodos para obtener el año, mes y día de esta clase ya están obsoletos y lo único que hacen es llamar a la clase java.util.Calendar para que esta se encargue de hacerlo (una delegación).

java.sql.Date: Esta clase hereda de java.util.Date y es la representación de la fecha cuando trabajamos con JDBC (Java DabaBase Connectivity), es decir, son los campos almacenados en una base de datos cuyo tipo es una fecha que puede o no incluir la hora, aunque la clase java.sql.Date siempre lo hace. Al igual que su clase padre, tiene una precisión de milisegundos, con la excepción que al mostrarla en la salida estándar con el formato por defecto solo muestra el día, mes y año. Hay que anotar también que para campos que almacenen solamente horas existen otras clases para manejarlos.

En resumen ambas clases, sólo se encargan de almacenar la cantidad de milisegundos que han pasado desde las 12 de la noche del primero de enero de 1970 en el meridiano de Greenwich. Aquí vienen dos puntos importantes:
a) Si la fecha que almacena cualquiera de las clases es menor a las 00:00:00 enero 1 de 1970 GMT, su valor el milisegundos será negativo.
b) La fecha es susceptible a la zona horaria. Por ejemplo en Colombia los milisegundos no se empiezan a contar desde enero 1 de 1970, sino a partir de las 19:00 de diciembre 31 de 1969. Esto es importante por que si transportamos una fecha relativa de una zona a otra, podemos llegar a tener problemas al confiar en los milisegundos que se tienen; además como la clase intenta representar el "Tiempo Universal Coordinado" (UTC) suma 0.9 segundos cada año para ajustar la diferencia entre el reloj atómico y la velocidad de rotación de la tierra. Esto se traduce en que muy dificilmente podemos basarnos en valores como 0 o 60000 para realizar validaciones, pues esos milisegundos no son controlables cuando creamos la instancia de una fecha, peor aún, los milisegundos no son ni siquiera iguales para la misma fecha en la misma zona horaria.

Ambas clases se pueden instanciar directamente mediante new(), pero la clase java.sql.Date necesita un parámetro en el constructor: el tiempo en milisegundos, así que las siguientes instrucciones son válidas:

java.util.Date fechaActual = new java.util.Date(); //Fecha actual del sistema
java.sql.Date inicioLocal = new java.sql.Date(0); //Milisegundo cero
 
//también se puede crear una instancia de java.util.Date con parámetros iniciales
java.util.Date otraFecha = new java.util.Date(1000); //El primer segundo a partir del inicio

Prueba a imprimir cada uno de estos valores y fíjate en la diferencia de formatos entre java.sql.Date y java.util.Date. Se puede pasar de java.sql.Date a java.util.Date de dos fomas, una de ellas es con una asignación simple:

java.util.Date utilDate = null;
java.sql.Date sqlDate = new java.sql.Date(0);
utilDate = sqlDate;
/* aunque es java.util.Date, 
 si la imprimes tendrá el formato de java.sql.Date, recordemos que java.sql.Date hereda de 
 java.util.Date */
System.out.println(utilDate);

También se pueden tomar los milisegundos de java.sql.Date y pasarlos al constructor de java.util.Date:

java.util.Date utilDate = null;
java.sql.Date sqlDate = new java.sql.Date(0);
utilDate = new java.util.Date(sqlDate.getTime());
//esta vez se mostrará con el formato de java.util.Date
System.out.println(utilDate);

Para pasar de java.util.Date a java.sql.Date se deben tomar los milisegundos de la primera y pasarlos al constructor de la segunda:

java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
//Con formato de java.sql.Date
System.out.println(sqlDate);

Para comparar fechas usamos el método compareTo() que internamente compara los milisegundos entre ellas usando directamente los métodos getTime() de ambas clases.

java.util.Date utilDate = new java.util.Date(); 
java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
if (utilDate.compareTo(sqlDate) == 0){
   System.out.println("IGUALES"); 
}else{ 
   System.out.println("DIFERENTES"); 
}

O lo que es equivalente:

java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
if (utilDate.getTime() == sqlDate.getTime()){
  System.out.println("IGUALES");
}else{
  System.out.println("DIFERENTES");
}

2. Las clases Time y Timestamp.
Ambas clases pertenecen al API JDBC y son la encargadas de representar los campos de estos tipos en una base de datos. Esto no quiere decir que no se puedan usar con otros fines. Al igual que java.sql.Date, son hijas (heredan) de java.util.Date, es decir, su núcleo son los milisegundos.
La clase Time es un envoltorio de la clase java.util.Date para representar los datos que consisten de horas, minutos, segundos y milisegundos, mientras Timestamp representa estos mísmos datos más un atributo con nanosegundos, de acuerdo a las especificaciones del lenguaje SQL para campos de tipo TIMESTAMP.
Como ambas clases heredan del java.util.Date, es muy fácil pasar de un tipo de dato a otro; similar a la clase java.sql.Date, tanto Time como Timestamp se pueden instanciar directamente y su constructor tiene como parámetro el número de milisegundos; como es de imaginarse, cuando se muestra alguna de las clases mediante su método toString() se ven los datos que intentan representar; La clase Time sólamente muestra la hora, minutos y segundo, mientras timestamp agrega fracciones de segundo a la cadena.
Para convertir entre tipos de datos diferentes debemos usar los milisegundos de una clase y asignarlos a las instancias de las otras, y como la clase java.util.Date es superclase de todas, a una instancia de esta podemos asignar cualquiera de las otras, manteniendo los métodos de la clase asignada, es decir, si asignamos un Time a una java.util.Date, al imprimir se verá el mismo formato de la clase Time.
Con este código:

  java.util.Date utilDate = new java.util.Date(); //fecha actual
  long lnMilisegundos = utilDate.getTime();
  java.sql.Date sqlDate = new java.sql.Date(lnMilisegundos);
  java.sql.Time sqlTime = new java.sql.Time(lnMilisegundos);
  java.sql.Timestamp sqlTimestamp = new java.sql.Timestamp(lnMilisegundos);
  System.out.println("util.Date: "+utilDate);
  System.out.println("sql.Date: "+sqlDate);
  System.out.println("sql.Time: "+sqlTime);
  System.out.println("sql.Timestamp: "+sqlTimestamp);

Se obtiene la siguiente salida:

util.Date: Thu May 20 19:01:46 GMT-05:00 2004
sql.Date: 2004-05-20
sql.Time: 19:01:46
sql.Timestamp: 2004-05-20 19:01:46.593

Note que aún cuando todos los objetos tienen los mismos milisegundos el formato con el que se muestran dependen de la clase que realmente los contiene. Es decir, no importa que a un objeto del tipo java.util.Date se le asigne uno del tipo Time, al mostrar a través de la consola se invocará el método toString() de la clase time:

utilDate = sqlTime;
System.out.println("util.Date apuntando a sql.Time: ["+sqlTime+"]");
utilDate = sqlTimestamp;
System.out.println("util.Date apuntando a sql.Timestamp: ["+sqlTimestamp+"]");

Arroja:

util.Date apuntando a sql.Time: [19:29:47]
util.Date apuntando a sql.Timestamp: [2004-05-20 19:29:47.468]

Pero si en vez de solo apuntar, creamos nuevas instancias con los milisegundos los formatos con que se muestran son los mismos. Note que lo verdaderamente importante ocurre cuando creamos la instancia de java.util.Date usando los milisegundos del objeto sqlTime, pues aunque este último únicamente muestra horas, minutos y segundos, siempre ha conservado todos los datos de la fecha con que se creó.

utilDate = new java.util.Date(sqlTime.getTime());
System.out.println("util.Date con milisegundos de sql.Time: ["+utilDate+"]");
utilDate = new java.util.Date(sqlTimestamp.getTime());
System.out.println("util.Date con milisegundos de sql.Timestamp: ["+utilDate+"]");

Fíjese en el formato de salida:

util.Date con milisegundos de sql.Time: [Thu May 20 19:54:42 GMT-05:00 2004]
util.Date con milisegundos de sql.Timestamp: [Thu May 20 19:54:42 GMT-05:00 2004]

Para finalizar esta primera entrega veamos el código para mostrar la diferencia entre dos fechas en horas, minutos y segundos. Esta no es la mejor forma para hacerlo, pero cabe bien para mostrar de forma práctica todos los conceptos anteriormente estudiados.

 import java.util.HashMap;
 import java.util.Map;
 public class Prueba {
  public static Map getDiferencia(java.util.Date fecha1, java.util.Date fecha2){
   java.util.Date fechaMayor = null;
   java.util.Date fechaMenor = null;
   Map resultadoMap = new HashMap();
 
   /* Verificamos cual es la mayor de las dos fechas, para no tener sorpresas al momento
    * de realizar la resta.
    */
   if (fecha1.compareTo(fecha2) > 0){
    fechaMayor = fecha1;
    fechaMenor = fecha2;
   }else{
    fechaMayor = fecha2;
    fechaMenor = fecha1;
   }
 
  //los milisegundos
   long diferenciaMils = fechaMayor.getTime() - fechaMenor.getTime();
 
   //obtenemos los segundos
   long segundos = diferenciaMils / 1000;
 
   //obtenemos las horas
   long horas = segundos / 3600;
 
   //restamos las horas para continuar con minutos
   segundos -= horas*3600;
 
   //igual que el paso anterior
   long minutos = segundos /60;
   segundos -= minutos*60;
 
   //ponemos los resultados en un mapa :-)
   resultadoMap.put("horas",Long.toString(horas));
   resultadoMap.put("minutos",Long.toString(minutos));
   resultadoMap.put("segundos",Long.toString(segundos));
   return resultadoMap;
}
 
  public static void main(String[] args) {
   //5:30:00 de Noviembre 10 - 1950 GMT-05:00
   java.util.Date fecha1 = new java.util.Date(-604070999750L);
 
   //6:45:20 de Noviembre 10 - 1950 GMT-05:00
   java.util.Date fecha2 = new java.util.Date(-604066478813L);
 
   //Luego vemos como obtuve esas fechas
   System.out.println(getDiferencia(fecha1, fecha2));
  }
}

DIFERENCIA ENTRE FECHAS
Fecha1: Fri Nov 10 05:30:00 GMT-05:00 1950
Fecha2: Fri Nov 10 06:45:21 GMT-05:00 1950
{segundos=20, horas=1, minutos=15}

Notas:
1. Existe un error de un segundo, lo cual no sucede cuando trabajamos con fechas posteriores a 1970, ¿Por qué?.
2. Este procedimiento funciona igual para todos los hijos de java.util.Date: java.sql.Date, java.util.Time y java.util.Timestamp.
3. Todos los ejemplos los hemos hecho creando nuevas instancias de las clases. He omitido el traer información desde una base de datos para no complicar el código; pero todo lo que hemos hecho debe funcionar igual de ambas formas (con base de datos y usando constructores).
En la próxima entrega veremos como realizar operaciones completas entre fechas utilizando las clases Calendar y GregorianCalendar.

Si no se indica lo contrario, el contenido de esta página se ofrece bajo Creative Commons Attribution-ShareAlike 3.0 License