Strptime: No Way Home – come si processano data e ora con la strptime in C e C++
Stephen Strange: Peter, a cosa devo il piacere? Peter Parker: Mi dispiace disturbarla, signore. Stephen Strange: Abbiamo salvato mezzo universo insieme, puoi evitare di chiamarmi signore. Peter Parker: Stephen... Stephen Strange: Suona strano ma te lo concedo.
Per la serie “non fare promesse se non puoi mantenerle”, rinvio al prossimo articolo (ma davvero?) la conclusione dell’argomento in corso, quello delle nuove keyword del C. Il fatto è che a volte mi trovo con dei problemi reali da risolvere, problemi che sono sempre un buon spunto per la scrittura. Ad esempio, ultimamente mi sono scontrato con uno dei rompicapo “classici” della programmazione, la gestione di data/ora (da qui in avanti datetime) di una applicazione vera, con tutti gli annessi e connessi, come il formato e il timezone (uh, quest’ultimo ha provocato nel corso degli anni dei mostruosi mal di testa a innumerevoli colleghi, incluso il sottoscritto… maledetto timezone!).
Ebbene si, mi sono sentito un po’ come il Peter Parker dell’ottimo Spider-Man: No Way Home, alle prese con i suoi soliti problemi esistenziali, di identità, di grandi responsabilità… perché una applicazione deve funzionare sempre bene (sono un perfezionista) e, quindi, deve girare bene a casa tua, nel tuo ufficio e, soprattutto, anche nel resto del mondo, e questo è vero solo quando i datetime sono stati gestiti come si deve. Per questo ci sto sempre attento, e non aspetto che si presenti l’errore per risolverlo: “prevenire è meglio che curare” (e “non ci sono più le mezze stagioni”, e “si stava meglio quando si stava peggio”, e…, non facciamoci mai mancare i luoghi comuni).
E veniamo al dunque: capita molto frequentemente di dover processare un datetime che ti è arrivato, in formato “human readable”, attraverso i più svariati canali (dall’interfaccia utente, dalla rete, ecc.). Fortunatamente i comitati ISO si sono occupati anche dei formati delle date (se ne occupano, normalmente, tra una nuova versione del C++ e la successiva, ah ah ah), e quindi possiamo usare, ad esempio, lo standard ISO 8601, per essere sicuri di riuscire a interpretare (quasi) qualsiasi formato (ma non vi preoccupate: ci sarà sempre qualcuno che non ama gli standard e vi proporrà qualche formato “custom” super-complicato, tanto per complicare anche la vostra vita). Ovviamente, quando ci arriva un datetime dobbiamo trasformarlo da “human readable” a “machine readable”, per poterlo poi trattare nel codice.
Un metodo abbastanza classico e di uso quasi universale (per fortuna) è trasformarlo in una struttura dati di tipo struct tm che contiene tutti i campi che ci necessitano per processare il datetime in una applicazione (e/o usare direttamente il tempo di riferimento dei sistemi POSIX, il numero di secondi passati da Epoch, ma questa è un altra storia).
Un modo tipico di processare il datetime è quello di usare la funzione standard della libc strptime(3) (nonché la sua versione speculare, la strftime(3)). E, a questo punto, direi di vedere direttamente un esempio per chiarire meglio il meccanismo: vai col codice!
#define _XOPEN_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> // main() - funzione main int main(void) { // decodifico il datetime con strptime(3) struct tm tm; memset(&tm, 0, sizeof(struct tm)); strptime("2022-01-21 14:29:44", "%Y-%m-%d %H:%M:%S", &tm); printf("il datetime sorgente è: 2022-01-21 14:29:44\n"); // mostro i campi della struct tm printf("\nla struttura tm corrispondente è:\n"); printf("tm_sec = %d\n", tm.tm_sec); /* Seconds (0-60) */ printf("tm_min = %d\n", tm.tm_min); /* Minutes (0-59) */ printf("tm_hour = %d\n", tm.tm_hour); /* Hours (0-23) */ printf("tm_mday = %d\n", tm.tm_mday); /* Day of the month (1-31) */ printf("tm_mon = %d\n", tm.tm_mon); /* Month (0-11) */ printf("tm_year = %d\n", tm.tm_year); /* Year - 1900 */ printf("tm_wday = %d\n", tm.tm_wday); /* Day of the week (0-6, Sunday = 0) */ printf("tm_yday = %d\n", tm.tm_yday); /* Day in the year (0-365, 1 Jan = 0) */ printf("tm_isdst = %d\n", tm.tm_isdst); /* Daylight saving time */ // ricostruisco il datetime con strftime(3) char buf[256]; strftime(buf, sizeof(buf), "%d %b %Y %H:%M", &tm); printf("\nil datetime ricostruito è: %s\n", buf); exit(EXIT_SUCCESS); }
E questo codice, una volta compilato ed eseguito, da il seguente risultato:
il datetime sorgente è: 2022-01-21 14:29:44 la struttura tm corrispondente è: tm_sec = 44 tm_min = 29 tm_hour = 14 tm_mday = 21 tm_mon = 0 tm_year = 122 tm_wday = 5 tm_yday = 20 tm_isdst = 0 il datetime ricostruito è: 21 Jan 2022 14:29
Tutto questo è molto semplice: si processa la data “human readable” in formato ISO 8601 con strptime(3), si ottiene una struct tm corrispondente e, passando questa struttura a strftime(3), si può riformattare il datetime (anche con un formato diverso, se necessario). Inutile ricordare che strptime(3) appartiene a una famiglia di funzioni che fanno capo a time(2), con cui si possono fare un sacco di cose interessanti (ma, magari, ne parleremo un altra volta).
E adesso veniamo al problema reale che mi ha ispirato questo articolo: la strptime(3) è una notevole funzione, ma alcuni formati non li digerisce benissimo, e lo standard ISO 8601 (ahimè) ne prevede un sacco. Il caso con cui mi sono scontrato è un datetime come il seguente che mi arrivava contenuto in un messaggio da processare:
2022-01-21T14:29:44.278Z
questo datetime è corretto da un punto di vista formale, anche se è poco usuale visto che contiene i millisecondi e, a causa di questo, il trattamento con strptime(3) a volte funziona e a volte no. Questo non è, ovviamente, un comportamento accettabile, e una possibile soluzione (sempre che proprio non vi servano anche i millisecondi) potrebbe essere quella di non usare strptime(3) e di usare, invece, scanf(3). Vediamo come:
#define _XOPEN_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> // prototipi locali void myStrptimeScanf(const char *s, const char *format, struct tm *tm); // main() - funzione main int main(void) { const char *datetime; struct tm tm; char buf[256]; // decodifico e ricostruisco il datetime con strptime(3) e strftime(3) // datetime = "2022-01-21T14:29:44.278Z"; myStrptimeScanf(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm); strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); exit(EXIT_SUCCESS); } // myStrptimeScanf() - funzione wrapper per strptime(3) void myStrptimeScanf( const char *s, // datetime sorgente const char *format, // formato del datetime sorgente struct tm *tm) // struct tm destinazione { memset(tm, 0, sizeof(struct tm)); int year, mon, mday, hour, min; float sec; sscanf(s, "%d-%d-%dT%d:%d:%fZ", &year, &mon, &mday, &hour, &min, &sec); tm->tm_year = year - 1900; // anno da 1900 tm->tm_mon = mon - 1; // 0-11 tm->tm_mday = mday; // 1-31 tm->tm_hour = hour; // 0-23 tm->tm_min = min; // 0-59 tm->tm_sec = (int)sec; // 0-60 }
E questo codice, una volta compilato ed eseguito, da il seguente risultato:
datetime: 2022-01-21T14:29:44.278Z - datetime ricostruito: 21 Jan 2022 14:29:44+0000
Questo metodo funziona ma non è molto flessibile, visto che il tipo di data accettato è solo quello dell’esempio, quindi dovremmo riscrivere la funzione per ogni tipo di datetime che ci può arrivare (e lo standard, come detto sopra, ne prevede molti). Tra l’altro il datetime proposto pur essendo abbastanza tipico (a parte i millisecondi) potrebbe a sua volta avere della varianti come queste:
2022-01-21T14:29:44Z 2022-01-21T14:29:44.278Z 2022-01-21T14:29:44+0100 2022-01-21T14:29:44+01.00 2022-01-21T14:29:44.278+0100 2022-01-21T14:29:44.278+01.00
dove ha un ruolo importante anche il maledetto timezone, che in questo caso è UTC (e ricordate: usare UTC è sempre una buona idea, può risparmiarvi un sacco di problemi).
Che fare, allora? La soluzione migliore è, direi, continuare a usare la strptime(3), che è molto flessibile (basta giocare con l’argomento <format>) e poi gestisce bene gli errori (cosa che non fa il codice con la scanf(3) mostrato qui sopra): “ma chi ce lo fa fare di riscrivere tutta la gestione degli errori per formati e/o valori sbagliati se lo fa già benissimo la strptime(3)? “. Il trucco da usare è molto semplice: si può aggiustare il datetime prima di passarlo alla strptime(3). E quindi, ripeto, se il nostro problema è solo quello dei millisecondi (che, dopo una rapida ricerca in rete, ho scoperto che è un problema non infrequente) possiamo operare nella seguente maniera:
#define _XOPEN_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> // prototipi locali char *myStrptime(const char *s, const char *format, struct tm *tm, size_t len); // main() - funzione main int main(void) { const char *datetime; struct tm tm; char buf[256]; // decodifico e ricostruisco il datetime con strptime(3) e strftime(3) // datetime = "2022-01-21T14:29:44.278Z"; myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19); strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); datetime = "2022-01-21T14:29:44+0100"; myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19); strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); datetime = "2022-01-21T14:29:44+01.00"; myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19); strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); datetime = "2022-01-21T14:29:44.278Z"; myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19); strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); datetime = "2022-01-21T14:29:44.278+0100"; myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19); strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); datetime = "2022-01-21T14:29:44.278+01.00"; myStrptime(datetime, "%Y-%m-%dT%H:%M:%S%z", &tm, 19); strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); exit(EXIT_SUCCESS); } // myStrptime() - funzione wrapper per strptime(3) char *myStrptime( const char *s, // datetime sorgente const char *format, // formato del datetime sorgente struct tm *tm, // struct tm destinazione size_t len) // lunchezza della parte data+ora+secondi { // estraggo la parte data+ora+secondi char part_one[32]; memcpy(part_one, s, len); part_one[len] = 0; // estraggo la seconda parte char part_two[32]; snprintf(part_two, sizeof(part_two), "%s", &s[strlen(part_one)]); // ricompongo le due parti e applico strptime(3) memset(tm, 0, sizeof(struct tm)); char mys[32]; char *mytimezone; if ((mytimezone = strchr(part_two, 'Z')) != NULL) { // caso speciale con Z finale snprintf(mys, sizeof(mys), "%s%s", part_one, mytimezone); return strptime(mys, format, tm); } else if ( (mytimezone = strchr(part_two, '+')) != NULL || (mytimezone = strchr(part_two, '-')) != NULL ) { // caso con timezone esplicito snprintf(mys, sizeof(mys), "%s%s", part_one, mytimezone); return strptime(mys, format, tm); } // caso non riconosciuto: ritorna errore return NULL; }
E questo codice, una volta compilato ed eseguito, da il seguente risultato:
datetime: 2022-01-21T14:29:44.278Z - datetime ricostruito: 21 Jan 2022 14:29:44+0000 datetime: 2022-01-21T14:29:44+0100 - datetime ricostruito: 21 Jan 2022 14:29:44+0100 datetime: 2022-01-21T14:29:44+01.00 - datetime ricostruito: 21 Jan 2022 14:29:44+0100 datetime: 2022-01-21T14:29:44.278Z - datetime ricostruito: 21 Jan 2022 14:29:44+0000 datetime: 2022-01-21T14:29:44.278+0100 - datetime ricostruito: 21 Jan 2022 14:29:44+0100 datetime: 2022-01-21T14:29:44.278+01.00 - datetime ricostruito: 21 Jan 2022 14:29:44+0100
Come avrete notato ho spezzato il datetime sorgente in due parti: la prima contiene data+ora+secondi, e può avere qualsiasi formato, tanto poi useremo la strptime(3) giocando con l’argomento <format>. Per rendere più generica la funzione ho aggiunto un nuovo argomento con la lunghezza della prima parte, così si può accettare (quasi) qualsiasi datetime. La seconda parte può contenere o non contenere i millisecondi (e se li contiene li tagliamo via, tanto nella struct tm non si possono mettere), e il timezone si processa con l’apposito formato “%z” che va bene per tutte le varianti mostrate sopra. Una volta ricomposta la stringa datetime sorgente possiamo chiamare la strptime(3) come se nulla fosse: una soluzione semplice per un problema solo apparentemente complesso (e ricordate: la soluzione più semplice è sempre la migliore, o non conoscete il Rasoio di Occam?).
Ok, per oggi può bastare, e prometto che ritorneremo (nel prossimo articolo) con l’argomento che avevamo lasciato pendente, quello delle nuove keyword del C (e se andate a controllare ho copiato/incollato esattamente la stessa promessa dello scorso articolo… la manterrò questa volta? ah ah ah).
Ciao, e al prossimo post!
https://italiancoders.it/strptime-no-way-home-come-si-processano-data-e-ora-con-la-strprime-in-c-e-c/