Prima o poi capita a tutti di doversi misurare con elaborazioni che coinvolgano calcoli di date e tempi. Talvolta tali calcoli sono fonte di fastidiose "emicranie", per nostra fortuna PHP ci supporta con una serie di funzioni dedicate alla gestione del tempo. In quest'articolo ne analizzeremo alcune terminando con un'applicazione pratica: la costruzione di un semplice calendario.
Seguendo il famoso adagio "l'unione fa la forza", sarà utile uno sguardo agli strumenti forniti dal database per decidere quando sia più opportuno farlo lavorare in luogo dell'engine PHP.
Procediamo nella trattazione rammentando che, al solito, la fonte principale di informazioni rimane la documentazione ufficiale reperibile all'indirizzo http://www.php.net/manual/en/ref.datetime.php.
Il timestamp
Il concetto fondamentale alla base della manipolazione del tempo con PHP è il timestamp ovvero il numero di secondi trascorsi dal 1 gennaio 1970 00:00:00 (la cosiddetta Unix Epoch) all'istante specificato. Per chiarire il concetto e senza voler apparire blasfemi, potremmo paragonare la Unix Epoch alla nascita di Cristo nel comune calcolo del tempo. Essa costituisce il riferimento rispetto al quale vengono espresse le date sia in senso anteriore, a.C., che posteriore, d.C.. Analogamente valori negativi del timestamp rappresentano date precedenti la Unix Epoch, mentre valori positivi rappresentano date successive.
L'intervallo di valori va' tipicamente dal 13 dicembre 1901 20:45:54 al 19 gennaio 2038 03:14:07 che corrispondono al minimo e massimo valore rappresentabili mediante un numero intero a 32 bit con segno. Purtroppo non tutte le piattaforme trattano il timestamp allo stesso modo, in particolare chi usa Microsoft Windows si troverà limitato ai valori positivi ovvero all'intervallo 1 gennaio 1970, 19 gennaio 2038. L'utilizzo di questa grandezza semplifica sicuramente le elaborazioni, al termine sarà però necessario convertire i risultati in una rappresentazione a noi più comprensibile e familiare.
Come prima funzione prendiamo in considerazione time(), essa fornisce lo unix timestamp al momento della sua esecuzione. L'istruzione seguente stamperà il numero di secondi dalla unix epoch ad adesso sabato 19 marzo 2005 07:03:53, momento in cui sto scrivendo queste righe:
<?php
// stamperà 1111212173
echo time();
?>
Per ottenere il valore del timestamp corrispondente ad una particolare data possiamo ricorrere alla funzione mktime(). Tale funzione prevede 7 argomenti interi nell'ordine: ore, minuti, secondi, mese, giorno, anno ed un valore che, se specificato, comunica alla funzione come comportarsi rispetto all'ora legale o DST Daylight Saving Time. Senza dilungarci troppo con il valore 1 indichiamo che il tempo vi ricade, 0 in caso contrario, -1 (valore di default) se non lo sappiamo e vogliamo che PHP si arrangi da solo.
Supponiamo di voler conoscere il timestamp corrispondente al 12 giugno 1987 12:30 procediamo così:
<?php
// stamperà 550492200
echo mktime(12,30,0,6,12,1987);
?>
I valori omessi a partire da destra verranno rimpiazzati automaticamente con i valori di data e tempo correnti. Nell'esempio che segue riprendiamo la data precedente tralasciando giorno e anno finali. Otterremo il timestamp del 19 giugno 2005 12:30 dove 19 e 2005 corrispondono ai valori relativi alla data corrente 19 marzo 2005 come più sopra ricordato.
<?php
// stamperà 1119177000
echo mktime(12,30,0,6);
?>
La funzione mktime() risulta molto robusta e flessibile, per questo viene largamente sfruttata per calcoli aritmetici sulle date e per la validazione delle medesime. Di seguito alcuni esempi chiarificatori:
<?php
// il timestamp di questo istante
echo mktime();
// il timestamp a 25 giorni dalla data specificata
$year = 2005;
$month = 6;
$day = 21;
echo mktime(0,0,0,$month,$day+25,$year);
// il timestamp corretto del 1 gennaio 2001
// malgrado l'errore nel mese e poi nel giorno
echo mktime(0,0,0,13,1,2000);
echo mktime(0,0,0,12,32,2000);
// il timestamp dell'ultimo giorno di febbraio 2003
// come giorno 0 di marzo 2003
echo mktime(0,0,0,3,0,2003);
// come giorno -31 di aprile 2003
echo mktime(0,0,0,4,-31,2003);
?>
Possiamo anche non esprimere l'anno mediante le canoniche 4 cifre tenendo presente che i numeri tra 0 e 69 saranno interpretati come 2000-2069, mentre quelli tra 70 e 99 come 1970-1999:
<?php
// timestamp del 1 gennaio 1977
echo mktime(0,0,0,1,1,77);
?>
Bene, siamo convinti che il timestamp sia una grande invenzione, ma i risultati ottenuti non sono molto belli a vedersi....proseguiamo e vediamo come sia possibile migliorarne la rappresentazione.
Rendiamo leggibili le date
Una delle funzioni più utili nella gestione di date e tempo è senza dubbio date(). Tale funzione permette di formattare il timestamp ricevuto secondo le impostazioni scelte dal programmatore basandosi su alcuni particolari caratteri. Eccone una breve lista, per l'elenco completo si veda la documentazione ufficiale.
carattere | significato |
---|---|
d | giorno del mese numerico 01-31 |
D | giorno della settimana in abbreviazione di 3 caratteri |
m | mese numerico 01-12 |
M | mese in abbreviazione di 3 caratteri |
F | mese in parola |
Y | anno a quattro cifre |
y | anno a due cifre |
H | ore 00-24 |
h | ore 00-12 |
i | minuti |
s | secondi |
Vediamo date() all'opera utilizzando la seguente impostazione per la rappresentazione "d-m-Y H:i:s" ovvero giorno-mese-anno ore:minuti:secondi in base a quanto riportato nella precedente tabella:
<?php
// stampa 01-01-2007 12:13:07
echo date ("d-m-Y H:i:s", mktime(12,13,7,1,1,2007));
?>
Se non si specifica il secondo parametro, cioè il valore relativo al timestamp, di default viene utilizzato il timestamp corrente. Considerando come istante attuale sabato 19 marzo 2005 11:39:14 ecco alcuni esempi:
<?php
// stampa Sat March 2005 11:39:14
echo date ("D F Y H:i:s");
// stampa 19.03.05
echo date ("d.m.y");
// stampa stampa 20050319
echo date ("Ymd");
// stampa 2005
echo date ("Y");
// stampa ecco: Sat March 2005 11:39:14
echo date ("ecco: D F Y H:i:s");
// stampa oggi: Sat March 2005 11:39:14
echo date ("oggi: D F Y H:i:s");
?>
Le combinazioni sono molte e possono soddisfare le più varie esigenze di rappresentazione, consiglio di tenere a portata di mano la tabella completa dei parametri di formattazione. Gli ultimi due esempi presentano all'interno della stringa di formattazione anche alcune parole che completano il risultato stampato. Come si noterà nel secondo caso risulta necessario l'escape di alcuni caratteri in quanto hanno un ben preciso significato per la funzione date(). Ad esempio il carattere "i" rappresenta i minuti. Per questa ragione, se non è proprio necessario, eviterei di complicare le cose stampando eventuali ulteriori parole al di fuori della funzione.
Immagino che qualcuno si stia già preoccupando perché i nomi di mesi e giorni compaiono in lingua inglese, un po' di pazienza e procedendo nell'articolo vedremo come affrontare il problema.
A questo punto vediamo alcuni esempi di come sia possibile utilizzare in combinazione mktime() e date().
<?php
// ieri
echo date ("d-m-Y",mktime(0,0,0,date("m"),date("d")-1,date("Y")));
// il mese prossimo
echo date ("d-m-Y",mktime(0,0,0,date("m")+1,date("d"),date("Y")));
// tra trenta giorni
echo date ("d-m-Y", mktime(0,0,0,date("m"),date("d")+30,date("Y")));
// tra 5 ore
echo date ("d-m-Y H:i:s", mktime(date("H")+5,date("i, s, m, d, Y")));
?>
In queste elaborazioni è consigliabile l'utilizzo di mktime anziché aggiungere semplicemente il numero di secondi al timestamp sia per problemi connessi all'ora legale che per la garanzia di controllo sul risultato fornito da tale funzione.
Altre utili funzioni
PHP ci fornisce un'apposita funzione per controllare la consistenza di una data ovvero checkdate(). La funzione riceve tre argomenti interi rispettivamente mese, giorno e anno e restituisce true o false a seconda che la data sia corretta o meno. Risulta particolarmente utile, ad esempio, nel validare i dati provenienti da un form.
<?php
// controlliamo la data 31 settembre 2003
// il risultato sarà bool(false)
var_dump(checkdate(9,31,2003));
// controlliamo se febbraio 2003 è
// bisestile il risultato sarà bool(false)
var_dump(checkdate(2,29,2003));
//si poteva anche procedere così
// ottenendo 0 (se fosse stato bisestile 1)
echo date("L",mktime(0,0,0,1,1,2003));
?>
Altra funzione spesso comoda è getdate(), essa riceve come argomento un timestamp e restituisce un array associativo con una serie di informazioni di seguito descritte. Se non specifichiamo alcun argomento utilizzerà come al solito il timestamp corrente.
<?php
// costruiamo l'array e stampiamolo
$oggi = getdate();
print_r($oggi);
/* il risultato sarà
Array (
[seconds] => 3
[minutes] => 2
[hours] => 17
[mday] => 19
[wday] => 6
[mon] => 3
[year] => 2005
[yday] => 77
[weekday] => Saturday
[month] => March
[0] => 1111248123
)
*/
?>
In altri termini l'array fornisce nell'ordine secondi, minuti, ore, numero del giorno, rappresentazione numerica del giorno della settimana (0 per domenica 6 per sabato), numero del mese, anno, numero che rappresenta il giorno dell'anno, stringa con il giorno della settimana, stringa con il mese, timestamp.
<?php
// stampa Saturday, 19 March
$oggi = getdate();
echo "{$oggi['weekday']}, {$oggi['mday']} {$oggi['month']}";
?>
Lo spazio è tiranno e per ora ci fermiamo qui. Vi do' appuntamento alla seconda parte dell'articolo dove vedremo altre funzioni, un po' meno note, affronteremo il problema della lingua ed infine realizzeremo un semplice calendario. Alla prossima settimana.
Proseguiamo illustrando altre funzioni di data e tempo e terminando con una loro applicazione pratica.
Una funzione un po' particolare, per questo non molto sfruttata, ma degna di menzione è strtotime() che cerca di convertire nel corrispondente timestamp una stringa contenente un'espressione inglese indicante una data. Se viene impostato un timestamp come secondo argomento il calcolo verrà effettuato rispetto a questo, altrimenti rispetto al timestamp corrente. Nel caso in cui la conversione non riesca la funzione restituirà -1. Come al solito gli esempi renderanno più chiaro quanto esposto, si considererà sempre come data corrente sabato 19 marzo 2005:
<?php
// stampa 19 03 2005
$tmsp = strtotime("now");
echo date('d m Y',$tmsp);
// stampa la prossima domenica: 27 03 2005
$tmsp = strtotime("next Sunday");
echo date('d m Y',$tmsp);
// aggiunge un mese stampa: 19 04 2005
echo date('d m Y',strtotime("+1 month"));
// aggiunge 3 giorni e 4 ore a 21-3-2005 17:30:22
// stampa: 24 03 2005 21:30:22
echo date('d m Y H:i:s',strtotime("+3 days 4 hours",mktime(17,30,22,3,21,2005)));
//stampa: 25 03 2005 04:00:00
$data = "2005-3-21";
$num_giorni = 4;
echo date('d m Y H:i:s',strtotime("$data +$num_giorni days 4 hours"));
?>
Le impostazioni locali
Vediamo ora come affrontare il problema precedentemente accennato dell'output in lingua inglese. La funzione strftime() riceve come primo parametro una stringa che indica il tipo di formattazione (analogamente a quanto visto con date()) e come secondo argomento un timestamp. La rappresentazione delle grandezze mediante stringhe, ad esempio i nomi dei mesi, si basa però sulle impostazioni locali del linguaggio settate con setlocale(). Ecco alcuni parametri di formattazione come al solito rimando alla documentazione ufficiale per un'elenco completo.
carattere | significato |
---|---|
%a | abbreviazione del giorno della settimana |
%A | nome completo del giorno della settimana |
%b | abbreviazione del nome del mese |
%B | nome del mese completo |
<?php
// impostiamo l'italiano
setlocale(LC_TIME,"it_IT");
// stampa sabato
echo strftime('%A');
// stampa sabato 19 marzo
echo strftime('%A %e %B');
//stampa sabado 19 marzo
setlocale(LC_TIME,"es_ES");
echo strftime('%A %e %B');
?>
Il codice precedente funzionerà se risulteranno installate sul sistema le impostazioni locali cui si fa' riferimento. Inoltre la stringa passata come secondo argomento a setlocale() non coincide su tutte le piattaforme. Spesso per rendere più portabili gli script si ricorre ad una soluzione meno elegante, ma che mette al riparo da sorprese. Essa consiste nel creare, ad esempio, degli array con le impostazioni relative ai linguaggi che si vogliono rendere disponibili.
<?php
$lang_month['it']['1'] = 'Gennaio';
$lang_month['it']['2'] = 'Febbraio';
$lang_month['it']['3'] = 'Marzo';
.............
$lang_weekday['it']['0'] = 'Domenica';
$lang_weekday['it']['1'] = 'Lunedì';
$lang_weekday['it']['2'] = 'Martedì';
.............
?>
In questo modo usando ad esempio i valori numerici forniti dalla funzione date() per mesi e giorni della settimana, rispettivamente ottenuti con i caratteri di formattazione "n" e "w", possiamo accedere alla corretta componente dell'array.
Quando ci vuole precisione
Come ultima funzione prendiamo in considerazione microtime() che fornisce lo unix timestamp più i microsecondi espresso nella forma "microsecondi secondi". Un classico esempio di utilizzo lo si trova nel calcolo del tempo di esecuzione di uno script. Una funzione simile alla seguente permette di ottenere un timestamp più preciso con l'aggiunta dei microsecondi:
<?php
function get_microtime()
{
//spezza il microtime in due variabili
list($misec,$sec) = explode(' ', microtime());
// aggiunge microsecondi e secondi
return ((float)$misec + (float)$sec);
}
$start = get_microtime();
/*
codice da eseguire
*/
$stop = get_microtime();
echo 'tempo di esecuzione: '. ($stop-$start);
?>
Per ottenere una stima attendibile conviene comunque ripetere l'esecuzione dello script più volte e calcolarne la media. Altro esempio di applicazione è l'inizializzazione del generatore di numeri pseudocasuali per ottenere un numero arbitrario:
<?php
srand(get_microtime());
$unique_number = rand();
?>
Si noti che a partire versione 4.2.0 di PHP l'argomento di srand() è opzionale, e per default viene impostato un valore casuale.
Costruiamo un calendario
Veniamo finalmente all'utilizzo pratico di alcune delle funzioni precedentemente illustrate. Per la costruzione del calendario ci avvarremo di un'altra funzione cal_days_in_month() disponibile dalla versione 4.1.0 di PHP e che fa' parte delle cosiddette "funzioni di calendario". Tale funzione fornisce, in breve, il numero di giorni per un fissato calendario, mese e anno.
Il primo argomento specifica il calendario cui fare riferimento, quello che noi adottiamo dal 1582 è il gregoriano, CAL_GREGORIAN, in onore di papa Gregorio XIII che sostituì il calendario giuliano, CAL_JULIAN, introdotto da Giulio Cesare nel 45 a.C.. Purtroppo, per aumentare la confusione, non tutti i paesi europei adottarono tale calendario immediatamente....il discorso ci porterebbe lontano concentriamoci sul nostro codice:
<?php
function print_calendar($month="",$year="")
{
// impostiamo la data attuale
$now = getdate(time());
// controlliamo mese e anno passati
if ( empty($month) OR empty($year) OR !is_numeric($month) OR !is_numeric($year) OR !@checkdate($month,1,$year) )
{
$month = $now['mon'];
$year = $now['year'];
}
// unix timestamp del primo giorno
// del mese e dell'anno ricevuti
$time = mktime(0,0,0, $month, 1, $year);
// genera l'array con le informazioni
$date = getdate($time);
// giorni totali per il mese e anno
$day_total = cal_days_in_month(CAL_GREGORIAN, $date['mon'], $date['year']);
//stampa mese e anno in oggetto
//in italiano come intestazione
setlocale(LC_TIME,"it_IT");
$mese_anno = strftime('%B',$date[0]). " " .$year;
echo "<table><tr><td colspan="7"><strong>$mese_anno</strong></td></tr>n";
// stampa le abbreviazioni dei giorni della settimana
echo "<tr><td>Do</td><td>Lu</td><td>Ma</td><td>Me</td> <td>Gi</td><td>Ve</td><td>Sa</td></tr>n";
for ($i = 0; $i < 6; $i++)
{
echo '<tr>';
for ($j = 1; $j <= 7; $j++)
{
$day_number = $j + $i*7 - $date['wday'];
//stampa la cella con il giorno
echo '<td';
if ($day_number > 0 AND $day_number <= $day_total)
{
// borda di rosso se è oggi
if ($day_number == $now['mday'] AND $month == $now['mon'] AND $year == $now['year'])
{
echo " style="border: 1px solid #cc0000;" ";
}
echo ">$day_number";
}
else
{
//stampa una cella vuota se non esiste il giorno
echo '> ';
}
echo '</td>';
}
echo "</tr>n";
if ($day_number >= $day_total AND $i != 6)
break;
}
echo "</table>n";
}
?>
Il codice precedente risulta volutamente non ottimizzato per maggiore comprensibilità. La prima parte controlla che i parametri passati siano accettabili altrimenti utilizza il mese e l'anno correnti. Si è fatto ricorso alla funzione is_numeric che controlla se i valori sono numeri o stringhe numeriche per poter utilizzare immediatamente dati provenienti da un form. Successivamente viene stampata l'intestazione del calendario, ricorrendo a setlocale() per l'italiano, con mese ed anno e l'abbreviazione dei giorni della settimana.
Il ciclo for più esterno for($i = 0; $i < 6; $i++) serve a costruire le righe del calendario che possono essere al massimo 6 nel caso il primo giorno del mese sia sabato ed il mese abbia più di 29 giorni.
Il ciclo più interno calcola e stampa il numero del giorno $day_number = $j + $i*7 - $date['wday']. Ogni riga riporta 7 giorni, per ognuna si parte da 1 più il numero di riga moltiplicato per 7. Questo sarebbe sufficiente se il primo giorno del mese fosse sempre domenica, ma non è così. Al valore va' quindi sottratto il numero del giorno della settimana da cui parte il mese che costituisce una sorta di "shift".
Se richiamiamo la funzione senza parametri, print_calendar(), otteremo il calendario del mese e giorno correnti. Se volessimo, invece, il calendario di giugno 2007 dovremo scrivere print_calendar(6,2007) ottenendo il seguente risultato:
giugno 2007 | ||||||
---|---|---|---|---|---|---|
Do | Lu | Ma | Me | Gi | Ve | Sa |
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Conclusioni
Come al solito scegliendo la funzione giusta al momento giusto PHP ci facilita il lavoro anche se gestire date e tempo risulta, in alcuni casi, abbastanza delicato. I risultati ottenuti talora dipendono dalle caratteristiche del sistema su cui gira PHP, lo si è accennato per il timestamp o per la funzione setlocale().
A complicare le cose contribuiscono, ad esempio, le differenze di fuso orario delle varie zone geografiche, i vari calendari, il DST. Se il nostro server si trova a Londra è probabile che segua il WET, Western European Time, coincidente con il GMT, Greenwich Mean Time (orario di riferimento) o con GMT + 1 durante il British Summer Time. Mentre noi utilizziamo il Central European Time, CET, pari a GMT + 1 o GMT + 2 se consideriamo l'ora legale.
Queste considerazioni finali non vogliono disarmare, ma solo esortare a prestare molta attenzione quando il quadro che abbiamo di fronte presenta complessità non sottovalutabili. In ultimo riporto che gli esempi dell'articolo sono testati su Linux Fedora Core 3, PHP 4.3.10, Apache 2.0.52.