Descrizione del progetto
Si vuole realizzare un programma in C++ per la gestione delle spese domestiche. Esse devono essere memorizzate in un archivio specificando: la data, l’importo in euro e una descrizione. Il programma deve fornire le seguenti funzionalità:
- Aggiunta di una nuova spesa.
- Visualizzazione di tutte le spese dell’archivio in ordine crescente di data.
- Visualizzazione delle spese sostenute in un certo periodo (ndr. dal, al), con calcolo del totale di quel periodo.
- Modifica di una spesa di cui si conosce la posizione occupata nell’archivio.
- Cancellazione fisica di una spesa di cui si conosce la posizione nell’archivio.
Soluzione proposta
La soluzione proposta di seguito è ottenuta tenendo conto delle seguenti scelte progettuali.
- Per realizzare l’archivio si opta per un file ad accesso diretto in quanto la realizzazione delle funzionalità 4. e 5. si complicherebbe oltremodo se non si ha la possibilità di posizionarsi direttamente su un record specifico di cui si conosce la posizione. A tal proposito si ricorda che C++ permette di posizionarsi direttamente in un punto specifico di un file binario in lettura e in scrittura, rispettivamente, con le istruzioni seekg() e seekp() già trattate in un altro articolo sui file binari: link.
- La scelta a. implica che, dovendo disporre di un accesso diretto ai record a partire dalla posizione da essi occupata, i record delle spese memorizzati nell’archivio devono avere tutti la stessa lunghezza in byte (record a lunghezza fissa).
- La realizzazione delle funzionalità 4. e 5. richiede che l’utente possa conoscere la posizione fisica occupata da ciascun record nel file dell’archivio. Per semplicità si sceglie di rendere l’ordine con cui i record vengono visualizzati a video coincidente con l’ordine fisico dei record memorizzati nel file dell’archivio.
- Per soddisfare il punto c. e semplificare anche la realizzazione delle funzionalità 1. e 2. si sceglie che l’aggiunta di ciascun record nell’archivio avvenga eseguendo i seguenti passi:
- Aggiunta del record in coda al file dell’archivio (scrittura in append).
- Ordinamento dei record dei file presenti nell’archivio, in ordine crescente di data.
- Per la realizzazione della funzionalità 5. si opta di operare prima la cancellazione logica del record e di realizzare la cancellazione fisica solo successivamente tramite la riscrittura in un file di appoggio dei record non cancellati logicamente.
I dati del problema vengono organizzati nelle strutture seguenti :
1 2 3 4 5 6 7 8 9 10 11 12 |
struct tData { int g; int m; int a; }; struct tSpesa { char descrizione[30]; float importo; tData data; bool cancellato = false; }; |
Funzionalità 1 – Aggiunta di una nuova spesa.
Questa funzionalità viene realizzata dalla funzione con prototipo: void aggiungiSpesa() che si procura in input il record di una nuova spesa, aggiunge il record prelevato in coda all’archivio e, infine, ordina i record memorizzati nel file dell’archivio in ordine crescente rispetto alla data. Tale funzione si serve dei sottoprogrammi realizzati con le seguenti funzioni:
- tSpesa chiediSpesa(), si procura in input il record di una nuova spesa e lo restituisce al chiamante (con un importo = 0 o una descrizione mancante, l’utente potrà annullare l’operazione di aggiunta di una nuova spesa); a sua volta essa utilizza la funzione tData chiediData(string mes).
- tData chiediData(string mes), si procura in input una data e fornisce al chiamante il record di una data valida. Questo sottoprogramma viene introdotto in quanto è utile e può essere riutilizzato per realizzare anche le altre funzionalità (vedi fig. della scomposizione funzionale).
- void ordinaPerData(), ordina in ordine crescente di data i record delle spese dell’archivio; a sua volta essa utilizza la funzione int confrontaDate(tData d1, tData d2).
- int confrontaDate(tData d1, tData d2), confronta le date d1 e d2 e restituisce un intero > 0 se la data d1 è maggiore di d2,
un intero < 0 se è minore, 0 se le due date coincidono. Questo sottoprogramma viene introdotto in quanto è utile e può essere riutilizzato per realizzare anche le altre funzionalità (vedi fig. della scomposizione funzionale).
Utilizzando le funzioni dei sottoprogrammi appena descritti, la funzionalità 1 può essere realizzata con il seguente codice C++:
1 2 3 4 5 6 7 8 9 10 11 |
void aggiungiSpesa() { tSpesa s = chiediSpesa(); //controlla che l'operazione non sia stata annullata dall'utente if(s.importo==0 || strcmp(s.descrizione, "")==0) return; //aggiunge il record in coda al file fstream f(NOME_ARCHIVIO, ios::app|ios::binary); f.write((char*) &s, sizeof(tSpesa)); f.close(); ordinaPerData(); } |
Funzionalità 2 – Visualizzazione di tutte le spese dell’archivio in ordine crescente di data.
Questa funzionalità viene realizzata dalla funzione con prototipo: void visualizzaSpese() che visualizza a video l’elenco formattato dei record delle spese presenti nel file dell’archivio. Tale funzione si serve dei sottoprogrammi realizzati con le seguenti funzioni:
- void stampaSpesa(tSpesa s), visaulizza il record di una spesa; a sua volta essa utilizza la funzione void stampaData(tData d).
- void stampaData(tData d), visualizza nel formato ‘gg/mm/anno’ il record della data fornito come parametro.
Utilizzando le funzioni dei sottoprogrammi appena descritti, la funzionalità 2 può essere realizzata con il seguente codice C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void visualizzaSpese() { tSpesa s; fstream f(NOME_ARCHIVIO, ios::in|ios::binary); //calcola in n. di record presenti nel file f.seekg(0, ios::end); int nRec = f.tellg()/sizeof(tSpesa); //visualizza ciascun record f.seekg(0, ios::beg); for(int i=0; i<nRec; i++) { f.read((char*) &s, sizeof(tSpesa)); cout << i+1 << ") "; stampaSpesa(s); cout << endl; } f.close(); } |
I sottoprogrammi utilizzati vengono sviluppati in fondo a questo articolo.
Funzionalità 3 – Visualizzazione delle spese sostenute in un certo periodo con calcolo del totale di quel periodo.
Questa funzionalità viene realizzata dalla funzione con prototipo: void visualizzaPeriodo() che visualizza tutti i record delle spese dell’archivio che si trovano all’interno dell’intervallo di tempo che si procura in input. Tale funzione si serve dei sottoprogrammi realizzati con le seguenti funzioni:
- void chiediPeriodo(tData& d1, tData& d2), si procura in input due date e fornisce al chiamante i record delle date valide dell’ inizio e della fine di un periodo attraverso il passaggio per riferimento dei due parametri delle date; a sua volta essa utilizza le funzioni tData chiediData(string mes) e int confrontaDate(tData d1, tData d2) già discusse.
- void stampaData(tData d), già discussa sopra.
Utilizzando le funzioni dei sottoprogrammi appena descritti, la funzionalità 3 può essere realizzata con il seguente codice C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
void visualizzaPeriodo() { tSpesa s; tData d1, d2; //si procura le date di inizio e fine periodo chiediPeriodo(d1, d2); system("cls"); cout << "---SPESE DEL PERIODO SCELTO---" << endl; cout << "Periodo dal "; stampaData(d1); cout << " al "; stampaData(d2); cout << endl; fstream f(NOME_ARCHIVIO, ios::in|ios::binary); //calcola in n. di record presenti nel file f.seekg(0, ios::end); int nRec = f.tellg()/sizeof(tSpesa); //visualizza i record compresi nel periodo float tot = 0; f.seekg(0, ios::beg); for(int i=0; i<nRec; i++) { f.read((char*) &s, sizeof(tSpesa)); if((confrontaDate(s.data, d1)>0 || confrontaDate(s.data, d1)==0) && (confrontaDate(s.data, d2)<0 || confrontaDate(s.data, d2)==0)) { stampaSpesa(s); cout << endl; tot = tot + s.importo; } } cout << "Totale periodo: euro " << setprecision(2) << fixed << tot << endl; f.close(); } |
Funzionalità 4 – Modifica di una spesa di cui si conosce la posizione occupata nell’archivio.
Questa funzionalità viene realizzata dalla funzione con prototipo: void modificaRecord() che modifica il record che si trova nella posizione ‘pos’ nell’archivio. L’utente potrà annullare l’operazione di modifica del record fornendo come posizione ‘0’. Tale funzione si serve dei sottoprogrammi realizzati con le seguenti funzioni:
- int chiediPosizione(string messaggio), richiede in input e restituisce al chiamante la posizione di un record che sia valida per l’archivio (oppure ‘0’ per annullare l’operazione), mostrando all’utente il ‘messaggio’ di richiesta fornito come parametro (ciò permetterà di riutilizzare la funzione anche per realizzare la funzionalità 5).
- void stampaSpesa(tSpesa s), già discussa sopra.
- tData chiediData(string mes), già discussa sopra.
Utilizzando le funzioni dei sottoprogrammi appena descritti, la funzionalità 4 può essere realizzata con il seguente codice C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
void modificaRecord() { char scelta; tSpesa s; int pos = chiediPosizione("Posizione del record da modificare ('0' per annullare)-> "); if(pos==0) return; fstream f(NOME_ARCHIVIO, ios::in|ios::out|ios::binary); //legge il record da modificare e lo visualizza f.seekg((pos-1)*sizeof(tSpesa), ios::beg); f.read((char*) &s, sizeof(tSpesa)); system("cls"); cout << "Record da modificare: " << endl; stampaSpesa(s); cout << endl << endl; //chiede qual è il campo da modificare e si procura il nuovo valore cout << "Scegli il campo da modifcare (1=data, 2=importo, 3=descrizione) -> "; cin>>scelta; cin.ignore(100, '\n'); switch(scelta){ case '1': s.data = chiediData("Nuova data -> "); break; case '2': cout << "Nuovo importo -> "; cin >> s.importo; break; case '3': cout << "Nuova descrizione -> "; cin >> s.descrizione; break; default: cout << "Attenzione, scelta sbagliata!" << endl; break; } //scrive il record modificato nel file if((scelta=='1') | (scelta=='2') | (scelta=='3')) { f.seekp((pos-1)*sizeof(tSpesa), ios::beg); f.write((char*) &s, sizeof(s)); } f.close(); } |
Funzionalità 5 – Modifica di una spesa di cui si conosce la posizione occupata nell’archivio.
Questa funzionalità viene realizzata dalla funzione con prototipo: void cancellaRecord() che cancella il record che si trova nella posizione ‘pos’ nell’archivio. L’utente potrà annullare l’operazione di cancellazione del record fornendo come posizione ‘0’. La cancellazione avviene prima ‘logicamente’ (settando a true il campo cancellato del record della spesa) e poi ‘fisicamente’. Tale funzione si serve dei sottoprogrammi realizzati con le seguenti funzioni:
- int chiediPosizione(string messaggio), già discussa sopra.
- void compattaArchivio(), ricompatta il file dell’archivio cancellando ‘FISICAMENTE’ i record che risultano cancellati ‘LOGICAMENTE’.
Utilizzando le funzioni dei sottoprogrammi appena descritti, la funzionalità 4 può essere realizzata con il seguente codice C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void cancellaRecord() { tSpesa s; int pos = chiediPosizione("Posizione del record da cancellare ('0' per annullare)-> "); if(pos==0) return; fstream f(NOME_ARCHIVIO, ios::in|ios::out|ios::binary); //si procura il record da cancellare f.seekg((pos-1)*sizeof(tSpesa), ios::beg); f.read((char*) &s, sizeof(tSpesa)); //cancella il record 'logicamente' s.cancellato = true; //riscrive il record nel file f.seekp((pos-1)*sizeof(tSpesa), ios::beg); f.write((char*) &s, sizeof(tSpesa)); f.close(); //cancella il record 'fisicamente' compattaArchivio(); } |
Qui sotto vengono inseriti i link al codice C++ dell’intero progetto che comprende anche lo sviluppo delle funzioni di tutti i sottoprogrammi individuati nella scomposizione funzionale e descritti sopra. I sorgenti sono ampiamente commentati per agevolarne la comprensione.
main.cpp my_header.h my_function.hPer completezza inserisco anche il link ad un video in cui spiego come realizzare il menù di scelta in C++ che è stato utilizzato nel file sorgente ‘main.cpp’ del progetto.