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!

lunedì 20 dicembre 2021

Double Express
come si comparano i double e i float in C

Ponchia: Ma non ti rovini i denti con tutta quella cioccolata?
Teresa: Ma ho un amico che è dentista.
Ponchia: Cioè, uno che ha un amico dentista allora si deve rovinare i denti. E se io avessi un amico patologo cosa dovrei fare?

La programmazione è un viaggio, un lungo viaggio alla ricerca del codice perfetto... esattamente come il viaggio nostalgico e catartico descritto nel bel Marrakech Express, un film che, metaforicamente, ci mostra la fuga dalla realtà e dal trantran quotidiano di un gruppo di amici. Un film brillantissimo che è ambientato in un momento storico e spensierato che mette un po' di nostalgia visto l'andazzo attuale. Ma bando alla tristezza: siamo qui per trattare di codice di alta qualità! Guardiamo avanti...

...ma non ti rovini il codice con tutti quei double?...

Nell'ultimo articolo avevamo proseguito il nostro viaggio tra le nuove keyword del C99/C11 (abbiamo già visto la prima e la seconda parte, quindi toccherebbe la terza). Ma oggi voglio divagare un po', mi prendo una piccola pausa per cambiare di tema (sento già i sospiri di sollievo: ma siete così sicuri che non sarà una pizza questo nuovo argomento?).

E allora vediamo in po': mi sono accorto, mio malgrado, che nel nostro piccolo-grande mondo dei programmatori ci sono alcuni peccati originali veramente duri a morire. In alcuni articoli passati (ad esempio qui, qui o qui) ho cercato di smitizzarne un po', e oggi cercherò di scrivere la parola fine (si fa per dire...) su un argomento che, navigando in rete, ho notato che continua a generare molti dubbi, e non è che sia un tema poi così nuovo! E quindi, tanto per chiarire:

I DOUBLE (E I FLOAT) NON SI COMPARANO MAI USANDO L'OPERATORE "=="

e vi dirò di più: si dovrebbe anche diffidare degli operatori "<", "<=", ">", ">="  e "!="  ma questo proprio se vogliamo fare gli schizzinosi (e anche questo punto lo vedremo più avanti nell'articolo).

Come detto questa è una vecchia storia, vecchia come i Computer e il Software, ma è ancora sconosciuta a molti. Comunque, per i dubbiosi, facciamo cantare il codice, dove proveremo a comparare due numeri che sono certamente uguali (o no?):

#include <stdio.h>

// main() - funzione main
int main()
{
// a e b dovrebbero valere entrambi 1.3...
double a = (0.3 * 3.0) + 0.4;
double b = 1.3;

// ...e invece...
printf("a = %.32f\n", a);
printf("b = %.32f\n", b);

// ...ecco la prova del disastro!
if (a == b)
printf("i numeri a e b sono uguali!\n");
else
printf("i numeri a e b NON sono uguali...\n");

return 0;
}

ecco, questo semplicissimo codice stampa questo:

a = 1.29999999999999982236431605997495
b = 1.30000000000000004440892098500626
i numeri a e b NON sono uguali...

Sorpresa! a  e b  NON  sono uguali! Beh, in realtà non è una sorpresa, basta indagare un po su come sono rappresentati i numeri double e float in un computer, e come vengono trattati da compilatore e CPU, e quindi ragionare un po' sulle approssimazioni di calcolo... ma non è questa la sede per approfondire questi dettagli, non voglio annoiare nessuno, quindi vi rimando a una delle tante descrizioni che si trovano in rete, ad esempio questa.

Allora: abbiate on non abbiate letto la documentazione consigliata sopra, fidatevi: il problema esiste e, come anticipato più sopra, non è affatto una novità: per esempio il grande Donald Knuth (che cito spesso su queste pagine) ne parlava già nel suo mitico libro The art of computer programming  (Volume II, 1981), dove proponeva alcune interessanti soluzioni del problema. E allora, facendo tesoro delle soluzioni proposte da Knuth, ho scritto questo semplice codice che vi invito a compilare ed eseguire:

#include <stdio.h>
#include <math.h>
#include <stdbool.h>

// prototipi locali
bool isEqual(double a, double b, double myepsilon);

// main() - funzione main
int main()
{
// a e b dovrebbero valere entrambi 1.3...
double a = (0.3 * 3.0) + 0.4;
double b = 1.3;

// ...e invece...
printf("a = %.32f\n", a);
printf("b = %.32f\n", b);

// ...ecco la prova del disastro!
if (a == b)
printf("i numeri a e b sono uguali!\n");
else
printf("i numeri a e b NON sono uguali...\n");

// ma c'è una soluzione!
if (isEqual(a, b, 0.001))
printf("ma con la soluzione i numeri a e b sono uguali!\n");
else
printf("anche con la soluzione i numeri a e b NON sono uguali...\n");

return 0;
}

// isEqual() - funzione di comparazione per variabili double (ispirata dalle
// soluzioni di D.Knuth in "The art of computer programming" (vol.2, 1981))
bool isEqual(
double a, // primo double da comparare
double b, // secondo double da comparare
double myepsilon) // un numero piccolo per riferimento di comparazione
{
if (islessequal(
fabs(a - b),
myepsilon * (isgreater(fabs(a), fabs(b)) ? fabs(a) : fabs(b)))) {

// se sono (approssimativamente) uguali ritorna true
return true;
}

// se sono (approssimativamente) diversi ritorna false
return false;
}

Dopo l'esecuzione vedrete questo:

a = 1.29999999999999982236431605997495
b = 1.30000000000000004440892098500626
i numeri a e b NON sono uguali...
ma con la soluzione i numeri a e b sono uguali!

Ecco, la funzione isEqual() è un buon compromesso tra affidabilità e semplicità per eseguire comparazione tra double (e si può `facilmente scriverne una versione per i float). In realtà, il problema è più complesso di quello che sembra, e le soluzioni possibili sono molteplici, ma quella che vi ho appena proposto è interessante e flessibile (anche se alcuni puristi dicono che non è sufficiente).

Analizziamo il codice: per eseguire il confronto si usa un numero molto piccolo di riferimento che ho chiamato myepsilon per distinguerlo da eventuali epsilon definiti in alcuni linguaggi e compilatori come "numero più piccolo trattabile" (ad esempio nel C++ c'è: std::numeric_limits<T>::epsilon): in realtà non abbiamo bisogno di un epsilon assoluto  ma possiamo usarne uno definito da noi (da qui il nome myepsilon) in base alla precisione che ci serve (e per questo ho definito questa soluzione come flessibile). 

Notare che myepsilon non viene usato come comparatore assoluto ma come moltiplicatore, così la isEqual() adatta il suo comportamento ai numeri in uso (che potrebbero anche essere molto piccoli, più piccoli dello stesso myepsilon).

Avrete visto, poi, che non ho usato direttamente gli operatori ">" e "<=" per effettuare due comparazioni, ma ho invece usato due macro, fornite dalla glibc, che sono state scritte ad-hoc per eseguire queste operazioni con variabili floating-point: e visto che le abbiamo a disposizione usiamole! Sono sicuramente più affidabili degli operatori quando si eseguono operazioni in virgola mobile (anche perché tengono conto di eventuali operandi di tipo NaN). La lista di equivalenza che ci interessa è questa:

operatore macro corrispondente
-----------------------------------------
> int isgreater(x, y);
>= int isgreaterequal(x, y);
< int isless(x, y);
<= int islessequal(x, y);
!= int islessgreater(x, y);

E vi ricordo che, nel C++, le stesse macro hanno comportamento ed uso identici ma ritornano bool invece di int.

Comunque, dopo tutta questa sbrodolata su come fare e non fare le operazioni floating point, vi aggiungo un "consiglio da amico" che resetta il tutto (anzi: se avessi cominciato l'articolo così vi avrei risparmiato tutto il tempo della lettura). Tenetevi forte: la maniera migliore di fare le operazioni in virgola mobile è non farle proprio!

E mi spiego meglio: tornando con i piedi per terra e parlando di casi pratici, ad esempio la scrittura di Software industriale (un classico del C e C++) è sempre una buona idea trattare i dati (chessoio: misure di sensori: tensione, corrente, pressione e chi più ne ha più ne metta) come interi associati a una precisione: quindi il nostro Sottware conterrà un parametro di configurazione che è la precisione, che verrà usata come divisore solo quando si dovranno presentare i dati su un monitor o si dovranno stampare dei report. Ad esempio: supponendo di avere una precisione 1000 (che corrisponde a 3 decimali), una tensione di 1,525V per noi sarà, internamente, un intero che vale 1525, che poi diventerà 1,525 dividendolo per la precisione.

Semplice, no? Eseguendo internamente tutti i calcoli usando solo interi si eliminano automaticamente i problemi descritti in questo articolo. Evidentemente ci sono casi in cui usare direttamente double e float  è indispensabile, ma nel Software industriale il trucco qui sopra è abbastanza usuale, ve l'assicuro.

Beh, per oggi può bastare. Abbiamo asfaltato un altro argomento dubbioso e prometto che ritorneremo (nel prossimo articolo) con l'argomento che avevamo lasciato pendente, quello delle nuove keyword del C (salvo ripensamenti dell'ultima ora, ah, ah, ah).

Ciao, e al prossimo post!