Riders of Strptime
come si processano data e ora con la strptime in C – pt.2
Membro del consiglio: Lascia che ti fermi qui, Otto. Otto: Si. Membro del consiglio: Quanto tempo lei e il suo team avete passato su questo algoritmo? Otto: Oh ragazzi, è difficile da dire esattamente... Quarantasei settimane. Ma l'abbiamo fatto principalmente di notte. Membro del consiglio: Quindi abbiamo speso un anno e una fortuna per un algoritmo che può capire che i poveri guidano Kia e i ricchi Mercedes?
Ebbene si, sono un perfezionista. È un difetto? È un pregio? Boh, non lo so. Anche Otto e Lars, i due coprotagonisti del bellissimo Riders of Justice sono dei perfezionisti, tanto da dedicare ben quarantasei settimane, alla scrittura di un algoritmo in grado di scoprire che i poveri guidano Kia e i ricchi guidano Mercedes (e li hanno licenziati: non c’è giustizia in questo mondo). Io, ad esempio, ho sempre tentato scrivere codice buono, a costo di sforare le tempistiche di realizzazione, piuttosto che scrivere in fretta codice che “basta che stia in piedi”. E se c’è qualche altro perfezionista tra i lettori (è un difetto frequente nei programmatori) mi capirà. Bravi Otto e Lars, io non vi avrei mai licenziato.
E cosa centra il discorso iniziale con questo articolo? Centra, centra… il problema è che pochi mesi fa ho scritto un post intitolato Strptime: No Way Home in cui proponevo una (spero) interessante versione “custom” della strptime(3), che è una ottima funzione della libc, ma ha qualche criticità d’uso e di funzionamento. In particolare mi ero soffermato su un problema reale che ho affrontato in un progetto, un problema sulla gestione di data/ora (da qui in avanti datetime) in formato ISO 8601 “ma con i millisecondi”, e proprio i millisecondi non piacciono molto alla strptime(3). Ora non mi sembra il caso di ripetere tutta la presentazione e gli esempi del vecchio articolo (basta rileggerlo, no?), credo che basti dire che è tutt’ora valido nei primi tre quarti (descrizione del tema, esempi vari e codice esplicativo) ma nell’ultimo quarto… c’e una funzione che avevo scritto ad-hoc, la myStrptime(), che 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).
(…e, tra l’altro, la myStrptime() contiene anche un piccolo bug, non grave direi, ma pur sempre un bug. Chissà se qualcuno se n’è accorto...)
Che fare allora? Correggere o modificare l’articolo originale 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” (questa che state leggendo) in cui vi invito a rileggere l’articolo “prima parte”, ricordandovi, però, di sorvolare sulla funzione finale myStrptime() prendendo per buona quella nuova che vi mostrerò tra poco.
Ok, si può fare!
Allora, riepiloghiamo: la strptime(3) funziona bene ed è ben adattabile a moltissimi tipi di datetime. Noi vogliamo farne una versione specializzata, che tratta qualche tipo in meno ma accetta i millisecondi, e già che ci siamo, approfittiamo per renderla un po’ più user-friendly (cambiando la sintassi d’uso: si perde in genericità ma si guadagna in facilità di sviluppo). E per quanto riguarda i tipi accettati bisogna coprire più casi rispetto alla myStrptime() originale: la famiglia di datetime accettata, sarà la seguente:
DATETIME UTC ------------ 2022-04-23T09:30:01Z 2022-04-23T09:30:01.278Z 2022-04-23T11:30:01+0200 2022-04-23T11:30:01+02.00 2022-04-23T11:30:01.278+0200 2022-04-23T11:30:01.278+02.00 20220423T09:30:01Z 20220423T09:30:01.278Z 20220423T11:30:01+0200 20220423T11:30:01+02.00 20220423T11:30:01.278+0200 20220423T11:30:01.278+02.00 2022-04-23T093001Z 2022-04-23T093001.278Z 2022-04-23T113001+0200 2022-04-23T113001+02.00 2022-04-23T113001.278+0200 2022-04-23T113001.278+02.00 20220423T093001Z 20220423T093001.278Z 20220423T113001+0200 20220423T113001+02.00 20220423T113001.278+0200 20220423T113001.278+02.00 DATETIME NON UTC ---------------- 2022-04-23T11:30:01 2022-04-23T11:30:01.278 20220423T11:30:01 20220423T11:30:01.278 2022-04-23T113001 2022-04-23T113001.278 20220423T113001 20220423T113001.278
Se qualcuno ha familiarità con il formato ISO 8601 noterà che l’obiettivo (ambizioso, direi) è coprire tutti i formati previsti del datetime UTC, il che non è poco! (nella versione del vecchio articolo si coprivano solo i primi 6 casi). E tratteremo anche i millisecondi! E, ciliegina sulla torta, copriremo anche alcuni formati non UTC. Sono ben 32 formati diversi, e scusate se è poco!
Allora, bando alle ciance: la nuova funzione l’ho ribattezzata isoStrptime() ed è questa (con relativo main() di esempio):
#define _XOPEN_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> // prototipi locali int isoStrptime(const char *s, struct tm *tm); // main() - funzione main int main(void) { char buf[256]; char datetime[32]; struct tm tm; strcpy(datetime, "2022-04-23T09:30:01Z"); if (isoStrptime(datetime, &tm) != -1) { strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); } else printf("ERROR!\n"); strcpy(datetime, "20220423T113001.278+0200"); if (isoStrptime(datetime, &tm) != -1) { strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S%z", &tm); printf("datetime: %-29s - datetime ricostruito: %s\n", datetime, buf); } else printf("ERROR!\n"); exit(EXIT_SUCCESS); } // isoStrptime() - funzione wrapper per strptime(3) int isoStrptime( const char *s, // datetime sorgente struct tm *tm) // struct tm destinazione { const char *format; // formato del datetime sorgente size_t len; // lunghezza della parte data+ora+secondi // analizzo la stringa sorgente if (strchr(s, '-') && strchr(s, ':')) { // appartiene al type/subtype: 2022-04-23T09:30:01Z format = "%Y-%m-%dT%H:%M:%S%z"; len = 19; } else if (strchr(s, '-') == NULL && strchr(s, ':')) { // appartiene al type/subtype: 20220423T09:30:01Z format = "%Y%m%dT%H:%M:%S%z"; len = 17; } else if (strchr(s, '-') && strchr(s, ':') == NULL) { // appartiene al type/subtype: 2022-04-23T093001Z format = "%Y-%m-%dT%H%M%S%z"; len = 17; } else if (strchr(s, '-') == NULL && strchr(s, ':') == NULL) { // appartiene al type/subtype: 20220423T093001Z format = "%Y%m%dT%H%M%S%z"; len = 15; } // controllo se la lunghezza della stringa sorgente è Ok char part_one[32]; if (strlen(s) >= len && strlen(s) <= 29) { // reset del tm prima di usarlo (questo è importante) memset(tm, 0, sizeof(struct tm)); // estraggo la parte data+ora+secondi memcpy(part_one, s, len); part_one[len] = 0; // eventualmente estraggo la seconda parte (il timezone) char mys[32]; char part_two[32]; if (strlen(s) > strlen(part_one) && (strlen(s) - strlen(part_one)) != 4) { // case con timezone snprintf(part_two, sizeof(part_two), "%s", &s[strlen(part_one)]); // ricompongo le due parti e applico strptime(3) char *mytimezone; if ((mytimezone = strchr(part_two, 'Z')) != NULL) { // caso speciale con Z finale snprintf(mys, sizeof(mys), "%s%s", part_one, mytimezone); } 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); } // uso strptime(3) per processare la nuova stringa if (strptime(mys, format, tm) != NULL) return 0; // strptime(3) Ok } else { // caso senza timezone (tolgo il %z dal format) char format_noz[32]; snprintf(format_noz, sizeof(format_noz), "%s", format); format_noz[strlen(format) - 2] = 0; // uso strptime(3) per processare la nuova stringa if (strptime(part_one, format_noz, tm) != NULL) return 0; // strptime(3) Ok } } // lunghezza non Ok oppure errore della strptime(3): ritorno errore return -1; }
Che ve ne pare? Questa mi dà già una buona impressione estetica (e di solito mi fido di questa impressione), ed è, evidentemente, molto più facile da usare: solo 2 parametri invece di 4! E non c’è bisogno di passare il format (che viene auto-costruito internamente), basta passare solo la stringa del datetime e la struct tm destinazione. Mi piace, e non contiene neanche il (piccolo) bug della myStrptime() citato sopra. Nel main() ho messo solo 2 esempi di uso (per renderlo meno pesante da leggere), ma sulla falsariga dei 2 esempi potete aggiungere gli altri 30, e magari anche qualche caso di data erronea (io l’ho fatto e funzionano tutti e 32, e anche gli errori sono ben rilevati).
Ebbene si, ora mi sento meglio, il peso da “funzione venuta male” si è affievolito, finalmente. E quindi per oggi può bastare, così potrò dedicare le prossime 46 settimane a creare qualche geniale algoritmo…
Ciao, e al prossimo post!