CalendarioL’importanza delle date e delle ore nel mondo è palese a tutti: calendari, impegni, scadenze, pianificazioni, ricorrenze e quant’altro sono all’ordine del giorno nella vita quotidiana di ognuno. Diventa, così, importante sapere come trattare questi oggetti nello sviluppo dei programmi.

L’argomento non è di semplice discussione: le date sono degli oggetti ostici, che assumono forme diverse a seconda del luogo geografico in cui ci troviamo o, semplicemente, a seconda del gusto di chi le deve trattare. E gli orari non sono certo da meno.

In questo articolo intendo trattare l’argomento Date e Ore dal punto di vista della programmazione Java, cercando di coprire tutti gli aspetti e mettendo in evidenza gli strumenti che tale linguaggio fornisce per il trattamento di questi oggetti.

Cominciamo questo percorso con una classe di fondamentale importanza, la cui documentazione deve sempre essere sotto mano quando si ha a che fare con le date e le ore: la classe java.util.Calendar. Questa è la classe “principale” per il trattamento di date e orari. Fino alla versione 1.1 del JDK, la classe preposta al trattamento delle date era java.util.Date, ma in seguito il suo utilizzo è stato deprecato a favore di Calendar. La classe è rimasta per questioni di compatibilità, ma solo due dei suoi costruttori sono sopravvissuti alla deprecazione: il costruttore vuoto e quello con un parametro long.

Cominciamo a vedere, quindi, la classe Calendar: innanzitutto notiamo che essa è una classe astratta, quindi non è istanziabile in prima battuta attraverso un costruttore. Quello che si deve fare per costruire un oggetto Calendar, quindi, è affidarci ad una sua sottoclasse, oppure ad uno dei suoi metodi getInstance() (questi metodi vengono detti “factory“, perchè consentono di ottenere un oggetto tramite un metodo statico della stessa classe, che si occupa della sua costruzione). A me, personalmente, piace lavorare con le sottoclassi. L’unica sottoclasse concreta conosciuta è GregorianCalendar, che offre tanta maneggevolezza, grazie ai suoi 7 costruttori. Vediamo, quindi, un esempio pratico su come ottenere un oggetto che rappresenti la data odierna:

GregorianCalendar gc = new GregorianCalendar();

Più facile di così, proprio si muore. Tramite questa semplicissima linea di codice abbiamo costruito un oggetto Calendar (nello specifico, un GregorianCalendar) contenente la data e l’ora attuali. Il problema, ora, è quello di poter lavorare con questo oggetto. Vediamo, ad esempio, di farci stampare a video la data e l’ora dell’oggetto “gc” appena costruito, nel formato classico “GG/MM/AAAA - HH:MM:SS”:

int anno = gc.get(gc.YEAR);
int mese = gc.get(gc.MONTH) + 1; // I mesi partono da 0
int giorno = gc.get(gc.DATE);
int ore = gc.get(gc.HOUR);
int min = gc.get(gc.MINUTE);
int sec = gc.get(gc.SECOND);
if (gc.get(gc.AM_PM) == gc.PM)
ore += 12; // voglio l'ora su 24 ore
String visualizza = "" + anno + "/";
if (mese < 10) {
   visualizza += "0" + mese;
} else {
   visualizza += "" + mese;
}
visualizza += "/";
if (giorno < 10) {
   visualizza += "0" + giorno;
} else {
   visualizza += "" + giorno;
}
visualizza += " - ";
if (ore < 10) {
   visualizza += "0" + ore;
} else {
   visualizza += "" + ore;
}
visualizza += ":";
if (min < 10) {
   visualizza += "0" + min;
} else {
   visualizza += "" + min;
}
visualizza += ":"
if (sec < 10) {
   visualizza += "0" + sec;
} else {
   visualizza += "" + sec;
}
System.out.println( visualizza );

Questo esempio, per quanto banale e migliorabile, ci fa capire tre cose:

  • La classe Calendar tratta i mesi a partire da 0 e non da 1, come ci si potrebbe aspettare. Questo è stato fatto, probabilmente, perchè l’informazione sul mese deriva da un array interno alla classe, che associa all’indice 0 il primo mese: Gennaio. In molti concordano nell’affermare che si sarebbe potuto prevedere un array più grande (di 13 elementi), lasciando vuota la prima posizione in modo da allineare Gennaio al valore 1, ma così non è e ci dobbiamo accontentare.
  • La classe GregorianCalendar è molto semplice da utilizzare: il suo metodo get(), che eredita da Calendar, consente di ottenere tutte le informazioni che desideriamo; basta guardare la documentazione della classe Calendar per rendersi conto di quanti sono i campi che mette a disposizione quel metodo.
  • Per quanto sia semplice (e migliorabile), lavorare in questo modo con le date per fare delle semplici formattazioni è scomodo e decisamente frustrante.

Se da un lato, quindi, risulta estremamente semplice ottenere le informazioni che ci interessano (tramite il metodo get()), dall’altro non è per nulla comodo effettuare visualizzazioni o semplici trasformazioni di formato delle date e delle ore. A questo, comunque, c’è un rimedio che Java mette a disposizione: la classe java.text.SimpleDateFormat. Questa classe permette di trattare con le date nel formato che più ci piace. Vediamo come si trasforma l’esempio precedente, utilizzando tale classe:

SimpleDateFormat sdf =
new SimpleDateFormat("dd/MM/yyyy - HH:mm:ss");
System.out.println( sdf.format( gc.getTime() ) );

Tutto un altro modo di vivere. Possiamo notare che siamo dovuti ricorrere al metodo getTime() di Calendar, che restituisce un oggetto di tipo Date. Anche questo è uno dei motivi di sopravvivenza di questa classe.

Se dobbiamo effettuare l’operazione inversa, allo stesso modo, possiamo utilizzare il nostro SimpleDateFormat per cercare di ottenere un oggetto Calendar. Se abbiamo una stringa, quindi, che rappresenta una data e la vogliamo “convertire” in un oggetto Calendar, le operazioni da effettuare sono le seguenti:

SimpleDateFormat sdf =
new SimpleDateFormat("dd/MM/yyyy - HH:mm:ss");
String miaData = "06/05/1986";
Calendar c = ( sdf.parse(miaData) ).getCalendar();

Questa operazione potrebbe sollevare una ParseException, nel caso in cui la stringa passata al metodo parse() non rappresenti una data convertibile.

A questo punto possiamo dedicarci a qualcosa di più sostanzioso ed ostico: i confronti fra le date e le ore. La maggior parte dei programmi che trattano con le date e con le ore devono effettuare dei confronti tra queste entità: pensiamo ad un allarme che deve sollevare un evento ad una certa ora, oppure ad un programma che deve estrarre tutte le fatture di un certo periodo di tempo (i casi sono praticamente infiniti). Chi ha una minima esperienza in questi casi sa che l’operazione di confronto fra due date (ma anche fra due ore) non è affatto una cosa semplice: bisogna tener conto dell’anno, quindi del mese all’interno dell’anno, quindi del giorno all’interno del mese, considerare quanti giorni ha il mese di interesse, aggiungiamoci l’eventualità di un anno bisestile… insomma, non si finisce più di fare calcoli e controlli. Per venire incontro alle esigenze dei programmatori, la classe Calendar prevede due comodi metodi: after() e before() che ritornano, entrambi, un valore booleano. Il primo ritorna true se l’oggetto che sto confrontando viene dopo quello passato come parametro, il secondo fa il contrario. Vediamo come vengono impiegati questi metodi:

GregorianCalendar data1 = new GregorianCalendar(2007, 11, 01);
GregorianCalendar data2 = new GregorianCalendar(2006, 11, 05);
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
if ( data1.before(data2) ) {
   System.out.println(sdf.format(data1.getTime()) +
   " viene prima di " +
   sdf.format(data2.getTime()));
} else {
   System.out.println(sdf.format(data2.getTime()) +
   " viene prima di " +
   sdf.format(data1.getTime()) +
   " oppure sono uguali");
}
if ( data1.after(data2) ) {
   System.out.println(sdf.format(data1.getTime()) +
   " viene dopo di " +
   sdf.format(data2.getTime()));
} else {
   System.out.println(sdf.format(data2.getTime()) +
   " viene dopo di " +
   sdf.format(data1.getTime()) +
   " oppure sono uguali");
}

Tutto semplice e comodo. Ma se il nostro programma prevede di effettuare un’operazione 25 giorni dopo una determinata data, che non conosciamo a priori? O se dobbiamo effettuare delle operazioni su delle date, ma senza spostarci dall’anno attuale? Anche in questo caso, abbiamo dei metodi molto comodi, che ci vengono forniti dalla classe GregorianCalendar: add() e roll(). Vediamo un esempio che ci fa capire la loro importanza:

// Il SimpleDateFormat usato è quello degli esempi precedenti.
// La data qui sotto impostata è 28 Novembre 2008 (10 = novembre)
GregorianCalendar gc = new GregorianCalendar(2008, 10, 28);
System.out.println( sdf.format(gc.getTime()) );
gc.add(gc.DATE, +33); // Aggiungo 33 giorni
System.out.println( sdf.format(gc.getTime()) );
gc.add(gc.YEAR, -1); // Tolgo 1 anno
System.out.println( sdf.format(gc.getTime()) );
gc.roll(gc.MONTH, +1); // Rollo in avanti di un mese
System.out.println( sdf.format(gc.getTime()) );

Eseguendo il codice postato sopra vedremo questo output:

28/11/2008
31/12/2008
31/12/2007
31/01/2007

Come si può vedere, Java ha fatto tutto il lavoro correttamente e noi non ci dobbiamo preoccupare di niente (aggiungere 33 giorni ad una data non è semplice: bisogna controllare quanti giorni ha il mese in corso, se si è a fine anno, se per caso è febbraio e l’anno è bisestile…). Il metodo roll() può spiazzare un po’ chi lo incontra per la prima volta: esso effettua l’aggiunta nel campo di interesse, senza apportare variazioni ai campi di ordine più alto (come l’anno, nell’esempio). Questo crea un effetto di rotazione all’interno dello stesso anno.

Se poi ci interessano altre informazioni, come, ad esempio, sapere se l’anno è bisestile o meno, abbiamo una serie di metodi che ci aiutano:

  • isLeap() ritorna true se l’oggetto rappresenta una data in un anno bisestile;
  • isLenient() ritorna true se l’interpretazione della data/ora che l’oggetto rappresenta può non essere ammissibile (è possibile, infatti, forzare un oggetto Calendar a rappresentare anche date e ore che non sono ammissibili, come il 32 di Gennaio. Vedi il metodo setLenient());
  • getActualMinimum() e getActualMaximum() ritornano, rispettivamente, il valore minimo ed il valore masimo per il campo specificato come parametro, prendendo in considerazione la data che l’oggetto rappresenta. Ad esempio è possibile sapere qual è il valore massimo per il campo “giorno del mese” assunto dal mese di febbraio nell’anno 2007, impostando una data coerente (esempio, 01/02/2007) e richiedendo getActualMaximum(gc.DATE);
  • getFirstDayOfWeek() ritorna il giorno della settimana che è considerato come primo giorno nel Paese indicato dal Locale (una classe particolare, che indica informazioni sul Paese in cui ci si trova), ad esempio: DOMENICA in America e LUNEDI’ in Italia.

Concludendo, quindi, abbiamo tanti strumenti che ci permettono di trattare le date e le ore: trasformazioni, visualizzazioni, calcoli e confronti non ci devono preoccupare perchè il corredo di classi e metodi a supporto è davvero enorme. Tutto quello che dobbiamo fare è concentrarci sul nostro lavoro e utilizzarli. Buona Programmazione a tutti.

Tags: