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.

mercoledì 27 aprile 2022

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.

...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: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! 

Nessun commento:

Posta un commento