Allocazione dinamica della memoria - Ingressi

Un piccolo parco giochi a pagamento di una località balneare gestisce il flusso di clienti fornendo un badge identificativo numerato a ciascun utente. La tariffazione avviene a tempo. A ciascun ingresso vengono pertanto associati l'orario di ingresso e di uscita, oltre che la data.

Tutti i dati vengono memorizzati in un file di testo. Questo è organizzato in modo da memorizzare i dati dei singoli utenti su righe separate. Le righe del file hanno il seguente formato:

gg/mm/aaaa nbadge h1:m1 h2:m2

dove:

  • gg/mm/aaaa è la data d'ingresso; gg è il giorno (il primo giorno del mese è 1), mm è il mese (gennaio è il mese numero 1, dicembre il 12) e aaaa è l'anno;
  • nbadge è il numero identificativo del badge, un intero compreso tra 1 e 1000;
  • h1:m1 e h2:m2 sono rispettivamente gli orari di ingresso e di uscita, nel formato ora:minuto; h1 e h2 rappresentano l'ora, e sono numeri interi compresi nell'intervallo [0, 23]; m1 e m2 rappresentano i minuti, e sono numeri interi nell'intervallo [0, 59].

Un esempio di file di input è il seguente:

01/04/2018 500 20:30 20:50
01/04/2018 510 20:30 21:00
01/05/2018 600 20:30 21:15
01/06/2018 610 20:30 21:30
01/07/2018 610 20:30 21:50

Lo stesso numero di badge può risultare associato a più ingressi differenti, purché gli orari non si sovrappongano nello stesso giorno. Come anticipato, la tariffazione avviene a tempo secondo il seguente tariffario:

  • fino a 20 minuti compresi: 3.5 euro
  • fino a 30 minuti compresi: 4 euro
  • fino a 45 minuti compresi: 4.5 euro
  • fino a 60 minuti compresi: 5.5 euro
  • oltre i 60 minuti: 7 euro (fino alla chiusura del parco)

Il parco apre tutti i giorni alle 19 e chiude alle 24. Il periodo di attività del parco va dal 1 aprile al 30 settembre. Il file memorizza i dati relativi ad un solo anno di gestione del parco.

Suggerimenti generali

Leggere tutto il testo prima di iniziare a risolvere i vari punti, in quanto alcune richieste potrebbero influenzare il modo di strutturare il programma.

In questo caso, per indirizzare il punto 2 è necessario caricare in memoria tutti i dati letti dal file, per poterne stampare i valori in ordine inverso! È la stessa situazione riportata in S5.9.3 dove, per poter stampare la sequenza di Fibonacci in ordine inverso, bisogna prima generarla e memorizzarla in un vettore, per poi stamparne i valori dall'ultimo al primo. Anche nel caso del punto 2 bisogna prima leggere tutti i dati dal file per poterne stampare alcuni valori in ordine inverso.

Per la lettura del file si può implementare una funzione con il seguente prototipo:

struct ingresso *leggi_file(FILE *fp, int *size);

dove:

  • fp è la struttura di tipo puntatore a FILE che identifica il file da leggere; il file deve essere già aperto in lettura prima di chiamare la funzione (usando fopen come in S8.2.1)
  • size è un intero passato per riferimento (vedi S6.6), che al termine dell'esecuzione della funzione conterrà il numero di dati letti
    • la rispettiva variabile deve essere dichiarata nel main, o comunque nella funzione che chiama leggi_file
  • la funzione restituisce un puntatore a struct ingresso
    • la struttura dovrà essere dichiarata per contenere tutti i dati necessari per ciascun ingresso (vedi sotto)
    • l'indirizzo restituito dalla leggi_file sarà quello del primo elemento di un vettore di strutture che viene allocato e riempito all'interno della funzione leggi_file

ATTENZIONE: la memoria necessaria per il vettore restituito dalla leggi_file dovrà essere allocata all'interno della funzione stessa tramite malloc/realloc (vedi S9.5) per due motivi:

  1. non è noto a priori quale è il numero di righe da leggere dal file, quindi non si può sapere da quanti elementi deve essere composto il vettore per contenere tutti i dati da leggere; questa informazione diviene nota quando sono state lette TUTTE le righe del file
  2. nella funzione non si dichiarerà un vettore (che - tra l'altro - potrebbe essere usato solo nella funzione), ma un puntatore a struct ingresso il cui indirizzo verrà inizializzato con la funzione malloc (vedi S9.5)

Un possibile esempio di dichiarazione della struttura struct ingresso può essere la seguente:

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

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

In caso di problemi a formulare la funzione richiesta, si veda la soluzione proposta.

1) Numero di ingressi

Determinare il numero d'ingressi registrati nel file. Stamparne il valore usando il formato:

[INGRESSI]
5

Suggerimenti

Per come è formulata la funzione leggi_file suggerita sopra, il numero di ingressi registrati è esattamente il valore del parametro size che viene "restituito" tramite passaggio per riferimento (vedi S6.6).

2) Stampa in ordine inverso

Stampare gli ultimi 10 badge presenti nel file, in ordine inverso. In altre parole, stampare per primo il badge contenuto nell'ultima riga del file, e via via gli altri fino a stampare per ultimo il badge contenuto nella riga posta a 10 righe dalla fine del file. Se il file contiene meno di 10 righe, stampare tutti e soli i badge presenti, sempre in ordine inverso.

Si usi il seguente formato:

[INVERSIONE]
610
610
600
510
500

Suggerimenti

Una soluzione elegante è quella di definire una funzione come la seguente:

void stampa_ultimi_inverso(struct ingresso *ingressi, int size, int num_da_stampare);

dove:

  • ingressi è il vettore di size elementi restituito dalla funzione leggi_file
  • num_da_stampare è il numero di badge da stampare a partire dall'ultimo, in ordine inverso

La funzione verrà richiamata con num_da_stampare pari a 10.

Per implementarla, basta realizzare un ciclo che stampi il valore del campo nbadge della struttura struct ingresso suggerita precedentemente, partendo dall'ultima struttura memorizzata nel file, e terminando il ciclo quando sono stati stampati num_da_stampare valori oppure sono stati stampati size valori se size < num_da_stampare.

3) Incasso mensile

Determinare gli incassi effettuati nei mesi di apertura. Stamparne il valore usando il seguente formato:

[INCASSO_MENSILE]
Aprile 0.00
Maggio 12.00
Giugno 5.50
Luglio 7.00
Agosto 0.00
Settembre 0.00

Si stampino i valori in virgola mobile con esattamente 2 cifre dopo la virgola.

Suggerimenti

Per risolvere questo punto bisogna tenere presente che:

  • l'incasso di un mese è la somma degli incassi di tutti gli ingressi in quel mese
  • l'incasso relativo ad un singolo ingresso dipende dalla durata dello stesso
  • la durata non è direttamente letta dal file, nel quale sono invece presenti gli orari di ingresso e di uscita

Pertanto, per calcolare l'incasso di ciascun mese bisogna procedere come segue:

  1. calcolare e memorizzare la durata della permanenza per ciascun ingresso
  2. calcolare la tariffa per ciascun ingresso in funzione della durata della permanenza
  3. scorrere tutti gli ingressi e sommare gli incassi relativi al mese corrispondente

Conviene quindi, innanzitutto, definire una funzione la quale, dati due orari (ore e minuti), calcoli la durata dell'intervallo corrispondente in minuti. Questo serve per determinare la durata dell'ingresso e quindi la fascia di tariffa da applicare. La funzione può essere dichiarata come segue:

int durata(int h1, int m1, int h2, int m2);

che restituisce il numero di minuti intercorsi tra i due orari h1:m1 e h2:m2. Il risultato può essere memorizzato nel campo permanenza all'interno della struttura struct ingresso utilizzata per conservare tutti i dati.

Successivamente, si può definire una funzione che restituisce la tariffa dovuta in funzione della durata della permanenza. Per esempio:

double calcola_tariffa(int tempo);

dove tempo è la durata in minuti della permanenza.

Infine, si può realizzare una funzione per calcolare l'incasso di ciascun mese di interesse. La funzione può essere dichiarata come segue:

void calcola_incassi_mensili(struct ingresso *elenco, int size, double *incassi);

dove:

  • ingressi è il vettore di size elementi restituito dalla funzione leggi_file
  • incassi è un vettore di double che conterrà l'incasso del mese corrispondente.
    • per poter usare il valore del mese come indice in un vettore in modo semplice, conviene usare per gli incassi un vettore di 12 elementi, uno per mese, anche per i mesi dei quali si sa che non c'è incasso (ricorda che il parco giochi è aperto solo un numero limitato di mesi durante l'anno!)

NOTA: per stampare il valore dell'incasso con esattamente 2 cifre decimali si usi lo specificatore %.2lf (vedi S2.12).

4) Incasso totale

Determinare l'incasso totale conseguito nell'anno. Stamparne il valore usando il seguente formato:

[INCASSO_TOTALE]
24.50

Si stampino i valori in virgola mobile con esattamente 2 cifre dopo la virgola.

Suggerimenti

L'incasso totale si può calcolare in due modi equivalenti.

Se sono già stati calcolati gli incassi mensili al punto 3, basta sommare il contenuto del vettore che memorizza tali incassi per ottenere l'incasso totale.

Altrimenti si possono sommare gli incassi dei singoli ingressi, che vanno calcolati come spiegato nella soluzione del punto 3.

5) Mese con più ingressi

Determinre il mese con il maggior numero d'ingressi. Stamparne il nome usando il seguente formato:

[MESE_MAX_INGRESSI]
Maggio

Suggerimenti

Per calcolare il mese con maggiori ingressi conviene calcolare l'istogramma degli ingressi relativi a tutti i mesi dell'anno. L'istogramma degli ingressi non è altro che il conteggio del numero di ingressi registrati per ciascun mese.

Memorizzando l'istogramma in un vettore, per trovare il mese con il maggior numero di ingressi basta trovare l'indice dell'elemento di valore maggiore all'interno del vettore.

Per semplicità nella gestione degli indici, è possibile utilizzare un vettore di 12 elementi per memorizzare l'istogramma. Questo significa che verranno memorizzati anche i conteggi dei mesi nei quali il parco giochi è chiuso, ma tali conteggi risulteranno pari a zero e non daranno quindi problemi per il calcolo del massimo.

Si può pertanto creare una funzione come

int calcola_mese_max_ingressi(struct ingresso *elenco, int size);

la quale prende in ingresso il vettore elenco, contente size elementi, il quale contiene i valori letti dal file. Può restituire un valore compreso tra 0 e 11 (estremi compresi) che indica il mese avente il maggior numero di ingressi.

Dal momento che bisogna stampare il nome del mese è sufficiente creare un vettore di stringhe contente i nomi dei 12 mesi, e usare il valore restituito dalla funzione sopra suggerita come indice nel vettore. Un esempio di dichiarazione e inizializzazione di vettore di stringhe è riportato in S9.4, mentre un esempio specifico di stampa del nome di un mese è riportato in SA.3.

6) Giorno con più ingressi

Determinare il giorno con il maggior numero d'ingressi. Stampare il giorno usando il seguente formato:

[GIORNO_MAX_INGRESSI]
01/05/2018

Giorno e mese vanno stampati usando esattamente 2 cifre. In caso il numero corrispondente presenti una sola cifra, si anteponga lo zero.

Suggerimenti

Per risolvere questo punto si può definire una funzione come la seguente:

struct data calcola_giorno_max_ingressi(struct ingresso *elenco, int size);

La funzione accetta l'elenco dei dati relativi agli ingressi letti con la funzione leggi_file, la quale contiene size elementi. Restituisce una struttura di tipo struct data, opportunamente dichiarata per contenere i dati di una data (giorno, mese e anno), che indica il giorno dell'anno con il maggior numero di ingressi.

Per trovare tale giorno, all'interno della funzione, si può calcolare l'istogramma degli ingressi nei vari giorni dell'anno e trovarne il massimo.

Per facilitare l'identificazione del giorno, consiglio di usare una matrice per la memorizzazione dell'istogramma, nella quale i due indici indicano rispettivamente il mese e il giorno. La matrice può essere dichiarata e i tutti i suoi elementi inizializzati a zero come segue:

int ingressi[12][31] = { {0} };

Con un semplice ciclo su tutti gli elementi del vettore elenco passato come primo argomento della funzione, si usano i valori di mese e giorno nella struttura struct ingresso come indici nella matrice, andando a incrementare il valore del giorno corrispondente. In pratica, invece di usare un singolo indice per il calcolo dell'istogramma, se ne usano due.

Per esempio, se è avvenuto un ingresso il giorno 10 aprile, si incrementrà di 1 il valore di ingressi[3][9], dove 3 indica il mese e 9 il giorno (si ricordi che matrici e vettori si indicizzano partendo da 0, quindi gennaio corrisponde all'indice 0, così come il primo giorno del mese corrisponde all'indice 0).

Dopodichè, con due cicli for annidati, uno che itera sui mesi da 0 a 11 e l'altro sui giorni da 0 a 31, si trovano i due indici dell'elemento della matrice avente il massimo valore di ingressi. Indichiamo rispettivamente con m_max e g_max gli indici corrispondenti al mese e al giorno. Per esempio, se m_max = 5 e g_max = 9, allora il giorno con il massimo numero di accessi sarà il 10 giugno, in quanto gennaio corrisponde all'indice 0, così come il primo giorno del mese corrisponde all'indice 0.

L'anno della data, invece, è uguale per tutti gli ingressi, e può quindi essere facilmente recuperato da uno qualsiasi degli elementi del vettore elenco (si suggerisce dal primo elemento, di indice 0, che sarà sicuramente presente nel vettore elenco).

Gil indici possono quindi essere usati per assegnare i valori alla struttura struct data che viene restituita dalla funzione.

NOTA: per stampare un numero intero usando esattamente 2 cifre e anteponendo lo 0 in caso di numeri con una sola cifra, usare lo specificatore di formato %02d (vedi S2.12).

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma. Il file contenente i test è ingressi.test. Per eseguire i test è necessario scaricare anche i seguenti file di dati che sono da salvare nella medesima directory del file di test:

Il comando da eseguire per il test è il seguente:

pvcheck -f ingressi.test ./a.out

Il file esempio.txt contiene invece i dati usati nell'esempio proposto in questa pagina, e può essere usato per testare il programma senza la necessità di utilizzare pvcheck.

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.