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.
...siamo qui per difendere i programmatori-perfezionisti. Qualcosa in contrario?... |
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:01Z2022-04-23T09:30:01.278Z2022-04-23T11:30:01+02002022-04-23T11:30:01+02.002022-04-23T11:30:01.278+02002022-04-23T11:30:01.278+02.0020220423T09:30:01Z20220423T09:30:01.278Z20220423T11:30:01+020020220423T11:30:01+02.0020220423T11:30:01.278+020020220423T11:30:01.278+02.002022-04-23T093001Z2022-04-23T093001.278Z2022-04-23T113001+02002022-04-23T113001+02.002022-04-23T113001.278+02002022-04-23T113001.278+02.0020220423T093001Z20220423T093001.278Z20220423T113001+020020220423T113001+02.0020220423T113001.278+020020220423T113001.278+02.00DATETIME NON UTC----------------2022-04-23T11:30:012022-04-23T11:30:01.27820220423T11:30:0120220423T11:30:01.2782022-04-23T1130012022-04-23T113001.27820220423T11300120220423T113001.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 localiint isoStrptime(const char *s, struct tm *tm);// main() - funzione mainint 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);}elseprintf("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);}elseprintf("ERROR!\n");exit(EXIT_SUCCESS);}// isoStrptime() - funzione wrapper per strptime(3)int isoStrptime(const char *s, // datetime sorgentestruct tm *tm) // struct tm destinazione{const char *format; // formato del datetime sorgentesize_t len; // lunghezza della parte data+ora+secondi// analizzo la stringa sorgenteif (strchr(s, '-') && strchr(s, ':')) {// appartiene al type/subtype: 2022-04-23T09:30:01Zformat = "%Y-%m-%dT%H:%M:%S%z";len = 19;}else if (strchr(s, '-') == NULL && strchr(s, ':')) {// appartiene al type/subtype: 20220423T09:30:01Zformat = "%Y%m%dT%H:%M:%S%z";len = 17;}else if (strchr(s, '-') && strchr(s, ':') == NULL) {// appartiene al type/subtype: 2022-04-23T093001Zformat = "%Y-%m-%dT%H%M%S%z";len = 17;}else if (strchr(s, '-') == NULL && strchr(s, ':') == NULL) {// appartiene al type/subtype: 20220423T093001Zformat = "%Y%m%dT%H%M%S%z";len = 15;}// controllo se la lunghezza della stringa sorgente è Okchar 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+secondimemcpy(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 timezonesnprintf(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 finalesnprintf(mys, sizeof(mys), "%s%s", part_one, mytimezone);}else if ( (mytimezone = strchr(part_two, '+')) != NULL ||(mytimezone = strchr(part_two, '-')) != NULL ) {// caso con timezone esplicitosnprintf(mys, sizeof(mys), "%s%s", part_one, mytimezone);}// uso strptime(3) per processare la nuova stringaif (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 stringaif (strptime(part_one, format_noz, tm) != NULL)return 0; // strptime(3) Ok}}// lunghezza non Ok oppure errore della strptime(3): ritorno errorereturn -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!
Nessun commento:
Posta un commento