Ingressi - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si assume che vengano effettuate le seguenti dichiarazioni:

struct data {
    int giorno, mese, anno;
};

struct ingresso {
    struct data data;
    int nbadge;
    int permanenza;   // in minuti
    double prezzo;
};

Lettura del file

La funzione per la lettura e l'elaborazione del file può essere come segue:

struct ingresso *leggi_file(FILE * fp, int *size)
{
    int h1, m1, h2, m2;
    int letti = 0;
    char linea[1024];
    int dim = 8;

    /* allocazione iniziale del vettore */
    struct ingresso *vect = malloc(dim * sizeof(struct ingresso));
    if (vect == NULL) {
        /* segnala al chiamante che non ho potuto allocare memoria */
        *size = 0;
        return NULL;
    }
    while (fgets(linea, sizeof(linea), fp) != NULL) {
        sscanf(linea, "%d/%d/%d %d %d:%d %d:%d",
               &(vect[letti].data.giorno), &(vect[letti].data.mese),
               &(vect[letti].data.anno), &(vect[letti].nbadge), &h1, &m1,
               &h2, &m2);
        vect[letti].permanenza = durata(h1, m1, h2, m2);
        vect[letti].prezzo = calcola_tariffa(vect[letti].permanenza);
        letti++;
        /* raggiunta la dimensione massima allocata per vect? */
        if (letti >= dim) {
            /* se si`, ne raddoppio la dimensione */
            dim *= 2;
            vect = realloc(vect, dim * sizeof(struct ingresso));
            if (vect == NULL) {
                return NULL;
            }
        }
    }
    *size = letti;
    vect = realloc(vect, letti * sizeof(struct ingresso));
    return vect;
}

La funzione inizialmente usa la malloc (presentata in S9.5) per allocare spazio per dim strutture di tipo struct ingresso. L'indirizzo dell'area di memoria allocata è assegnato al puntatore vect. Si ricordi che malloc alloca una quantità di memoria espressa in byte, quindi la sizeof (introdotta in S5.10) viene usata per determinare la dimensione di una singola struttura di tipo struct ingresso. Se vect vale NULL allora non è stato possibile allocare la memoria e la funzione ritorna, restituendo NULL per indicare al chiamante il fallimento della lettura.

Se l'allocazione iniziale della memora ha successo, il ciclo while usa la fgets per leggere una riga alla volta dal file. In ciascuna iterazione, la funzione sscanf viene usata per estrarre i dati dalla riga letta e per memorizzarli nell'area appena allocata, che viene acceduta come fosse un vettore tramite il puntatore vect (vedi equivalenza tra puntatori e vettori illustrata in S9.2).

La durata della permanenza e l'incasso vengono calcolati rispettivamente con le funzioni durata e calcola_tariffa, illustrate di seguito.

Se il numero di elementi memorizzati nel vettore vect eccede la dimensione del vettore dim, il valore di dim viene raddoppiato e la memoria allocata per vect viene ridimensionata con la realloc. Il ridimensionamento della memoria allocata non tocca il contenuto della memoria precedente, cosicché quanto è tato memorizzato rimane tale, e si ha spazio per memorizzare ulteriori dati.

Alla fine del ciclo, il valore di *size (il valore puntato dal puntatore size) viene posto pari al numero di dati letti (variabile letti). Si ricordi che size è un puntatore, che viene usato con il passaggio per riferimento al fine di permette alla funzione leggi_file di "restituire" il valore opportunamente modificato per riflettere il numero righe lette.

Infine, il vettore vect viene ridimensionato con realloc per contenere esattamente un numero di strutture pari a letti, e viene restituito l'indirizzo della memoria allocata e contenente i dati letti nella funzione.

Funzioni di supporto alla lettura

Durante la lettura vengono usate due funzioni che aiutano a calcolare ivalori da inserire nella struttura struct ingresso: la funzione durata e calcola_tariffa.

La durata può essere calcolata con una funzione come la seguente:

int durata(int h1, int m1, int h2, int m2)
{
    int min1 = (h1 * 60) + m1;
    int min2 = (h2 * 60) + m2;
    return min2 - min1;
}

I valori in ore e minuti vengono convertiti in minuti, e viene restituita la differenza tra l'orario h2:m2 e h1:m1.

NOTA: per svolgere i calcoli su date e orari, conviene convertirne il valore in una unità di misura che permetta di svolgere i calcoli tra singoli numeri interi. Nell'esempio, gli orari espressi in ore e minuti sono stati convertiti in minuti trascorsi dalla mezzanotte, che è sempre un numero intero di minuti.

Il calcolo della tariffa si può effettuare come segue:

double calcola_tariffa(int tempo)
{
    if (tempo <= 20) return 3.5;
    if (tempo <= 30) return 4.0;
    if (tempo <= 45) return 4.5;
    if (tempo <= 60) return 5.5;
    return 7.0;
}

È importante notare che non è elegante mischiare i dati (cioè i valori delle fasce orarie e delle tariffe) con il codice. Quindi è possibile impostare una soluzione alternativa e migliore che separa i due aspetti.

Scopri un esempio di soluzione alternativa »

1) Numero di ingressi

Il numero di ingressi corrisponde semplicemente al valore di size restituito dalla leggi_file illustrata più sopra.

2) Stampa in ordine inverso

Per mostrare come anche il problema più semplice possa ammettere diverse logiche di implementazione, vengono mostrate due possibili implementazioni della funzione stampa_ultimi_inverso, che possono essere usare in modo comletamente intercambiabile, dal momento che le funzioni accettano gli stessi parametri e producono lo stesso risultato a parità di dati in ingresso.

I parametri sono l'elenco di size ingressi, memorizzato in un vettore di strutture struct ingresso puntato da ingressi, e il numero di badge da stampare a partire dall'ultimo. Questo parametro, benchè non indispensabile poiché nel tutorial si richiede di stampare al massimo 10 valori, mostra come è possibile rendere parametrica e generica la funzione, facendo in modo che il numero di dati da stampare venga passato alla funzione, in modo da poterla usare per stampare un qualsiasi numero di badge.

La prima versione di stampa_ultimi_inverso inizia determinando il numero esatto di badge da stampare, verificando se size è minore di num_da_stampare, nel qual caso num_da_stampare viene posto uguale a size. In questo modo, se non ci sono num_da_stampare elementi da stampare, vengono stampati tutti, in numero pari a size. Dopodiché, il ciclo for viene eseguito un numero di volte pari a num_da_stampare, stampando con printf il badge a partire dall'ultimo, procedendo all'indietro man mano che il valore di i viene incrementato. In questa soluzione è importate gestire correttamente l'indice per indicizzare il vettore ingressi.

void stampa_ultimi_inverso(struct ingresso *ingressi, int size, int num_da_stampare)
{
    int i;
    if (size < num_da_stampare)
        num_da_stampare = size;
    for (i = 0; i < num_da_stampare; i++)
        printf("%d\n", ingressi[size - i - 1].nbadge);
}

La seconda versione utilizza un ciclo while per stampare i badge partendo dal fondo del vettore man mano che il valore di i viene incrementato ad ogni iterazione. Il ciclo while continua fintanto che i rimane minore di num_da_stampare e allo stesso tempo i rimane minore di size. Quando una delle due condizioni diviene falsa, non importa quale, il ciclo termina. Implicitamente, se size è minore di num_da_stampare la condizione che diventerà falsa sarà la seconda. Viceversa, ovvero se size è maggiore o uguale a num_da_stampare sarà la prima a far terminare il ciclo.

void stampa_ultimi_inverso(struct ingresso *ingressi, int size, int num_da_stampare)
{
    int i = 0;
    while ((i < num_da_stampare) && (i < size)) {
        printf("%d\n", ingressi[size - i - 1].nbadge);
        i++;
    }
}

3) Incasso mensile

Una possibile funzione che calcola l'incasso mensile è la seguente:

void calcola_incassi_mensili(struct ingresso *elenco, int size, double *incassi)
{
    int i;
    for (i = 0; i < size; i++) {
        incassi[elenco[i].data.mese - 1] += elenco[i].prezzo;
    }
}

I suoi parametri sono:

  • il vettore puntato da elenco, contenente size elementi, dove ogni elemento contiene il prezzo pagato e calcolato nella funzione leggi_file
  • il puntatore incassi, che passa alla funzione un vettore di 12 valori double, uno per mese, che verranno aggiornati con il totale dell'incasso mensile

Il vettore incassi sarà dichiarato nella funzione che chiama calcola_incassi_mensili come segue:

    double incasso_mensile[12] = { 0.0 };

La funzione non fa altro che sommare all'elemento giusto il valore del prezzo. L'elemento viene determinato usando come indice il valore del mese. Tale valore è decrementato di uno perchè i mesi letti dal file vanno da 1 a 12, mentre gli indici nel vettore devono andare da 0 a 11.

4) Incasso totale

Posto di aver calcolato gli incassi mensili risolvendo il punto 3, gli incassi annuali si possono calcolare con una funzione come la seguente:

double incasso_annuale(double *incasso_mensile)
{
    int i, incasso;
    for (i = 0; i < 12; i++) {
        incasso += incasso_mensile[i];
    }
    return incasso;
}

Questa funzione somma gli incassi anche dei mesi nei quali il parco giochi è chiuso, ma tali valori sono pari a zero e non modificano il valore dell'incasso.

prova-tu Prova a modificare la funzione per sommare solo gli incassi dei mesi nei quali il parco giochi è aperto.

prova-tu Come potrebbe essere modificata la funzione nel caso in cui i mesi di apertura non fossero consecutivi (per esempio, apertura da marzo a giugno e da settembre a novembre).

5) Mese con più ingressi

Per calcolare il mese con il maggior numero di ingressi si potrebbe usare una funzione come la seguente:

#define MESI_X_ANNO  (12)

int calcola_mese_max_ingressi(struct ingresso *elenco, int size)
{
    int i;
    int ingressi[MESI_X_ANNO] = { 0 };    /* per registrare gli ingressi */
    int max = 0;
    int mese_max;

    for (i = 0; i < size; i++) {
        ingressi[elenco[i].data.mese - 1]++;
    }
    /* cerco il massimo */
    for (i = 0; i < MESI_X_ANNO; i++) {
        if (ingressi[i] > max) {
            max = ingressi[i];
            mese_max = i;
        }

    }
    return mese_max;
}

Il primo ciclo for calcola l'istogramma degli ingressi nell'anno, contenuti nel vettore puntato da elenco di dimensione size (quello che viene prodotto dalla funzione leggi_file), memorizzandolo nel vettore ingressi di 12 elementi (in virtù del valore della macro MESI_X_ANNO).

Il secondo ciclo for trova l'indice dell'elemento maggiore nel vettore ingressi. Questo corrisponde all'indice mese_max del mese con più ingressi, che viene restituito alla funzione chiamante.

Per poter stampare il nome del mese, conoscendone l'indice, basta una istruzione del tipo

    printf("%s\n", NOMI_MESI[meseMaxIngr]);

dove meseMaxIngr è il valore restituito da calcola_mese_max_ingressi, e NOMI_MESI è dichiarato prima della funzione come segue:

char *NOMI_MESI[] = {
    "Gennaio", "Febbraio", "Marzo",
    "Aprile", "Maggio", "Giugno",
    "Luglio", "Agosto", "Settembre",
    "Ottobre", "Novembre", "Dicembre"
};

6) Giorno con più ingressi

Per calcolare la data con il maggior numero di ingressi si potrebbe usare una funzione come la seguente:

struct data calcola_giorno_max_ingressi(struct ingresso *elenco, int size)
{
    int i, g, m;
    /* per registrare gli ingressi di tutti i giorni dell'anno
     * per semplicita` e generalita` considero tutti e 12 i mesi */
    int ingressi[MESI_X_ANNO][GIORNI_X_MESE] = { {0} };
    int max = 0;
    int g_max, m_max;   /* indici del giorno e mese di massimo ingresso */
    struct data data;

    for (i = 0; i < size; i++) {
        data = elenco[i].data;
        ingressi[data.mese - 1][data.giorno - 1]++;
    }

    max = 0;
    for (m = 0; m < MESI_X_ANNO; m++) {
        for (g = 0; g < GIORNI_X_MESE; g++) {
            if (ingressi[m][g] > max) {
                max = ingressi[m][g];
                m_max = m;
                g_max = g;
            }
        }
    }
    data.anno = elenco[0].data.anno;   /* tutte le date dello stesso anno */
    data.mese = m_max + 1;
    data.giorno = g_max + 1;
    return data;
}

Viene calcolato l'istogramma degli ingressi a partire dai size elementi del vettore elenco passato come argomento.

L'istogramma è memorizzato nella matrice ingressi di 12 righe e 31 colonne, avendo avuto cura di definire le due macro:

#define GIORNI_X_MESE  (31)
#define MESI_X_ANNO  (12)

I due cicli for annidati cercano l'elemento di valore massimo nella matrice. Gli indici di tale elemento, memorizzati nelle variabili m_max e g_max per il mese e il giorno rispettivamente, indicano direttamente la data cercata.

Terminati i cicli for, vengono assegnati i campi di una struttura di tipo struct data per restituire il giorno desiderato.

Gli indici m_max e g_max vengono incrementati di 1 in quanto il loro valore è rispettivamente nell'intervallo [0, 11] e [0, 30], mentre le date vengono poi stampate con valori che partono da 1, sia per i giorni che per i mesi.

Il valore dell'anno viene preso dalla struttura memorizzata come primo elemento del vettore, dal momento che tutti gli anni sono gli stessi.