Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

domenica 23 gennaio 2022

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 StrangeSuona 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).

...preparati a non sapere più neanche che giorno è oggi...

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!

P.S.

Questo articolo contiene (spero) argomenti e spiegazioni interessanti. Ma la funzione finale proposta, la myStrptime(), già` al tempo della pubblicazione non mi  convinceva più di tanto (non mi dava una buona impressione neanche “esteticamente”, sapete com'è: sesto senso del programmatore). Che fare allora? Correggere o modificare questo articolo per migliorarlo facendo finta di niente? Oppure pubblicare un remake? (un remake dopo tre mesi? Nel cinema di solito si aspetta un po’ di più…). Ok, alla fine ho optato per fare una “seconda parte”, che potete trovare qui. Ciao di nuovo!