Lavorare con le date Andrea Asta
Scopo del progetto Vogliamo riuscire a creare un programma in grado di lavorare con le date, per simulare una specie di calendario. Il problema iniziale sarà quello di determinare il giorno della settimana partendo da tre informazioni: Giorno del mese Mese Anno Infine allargheremo maggiormente il progetto per poter visualizzare un intero calendario.
Risorse disponibili Per lavorare al progetto, è disponibile il linguaggio C++ e queste sue funzionalità Tipi di dati semplici Istruzioni di selezione Cicli Operatori matematici Funzioni con passaggio per valore Funzioni con passaggio per indirizzo Librerie iostream, conio, ctype, stdlib, math Ci si serve inoltre del calendario di sistema per effettuare tutti i test.
Problema di base L’utente fornirà da tastiera tre variabili di tipo intero: int giorno, mese, anno; cin >> giorno >> mese >> anno; Il risultato finale sarà un cout che indica il giorno della settimana della data specificata. La stampa sarà gestita per comodità con un’istruzione switch(). Ad esempio, inserendo come input 17, 12, 2004 (corrispondente alla data 17 dicembre 2004), l’output da visualizzare su schermo è “Venerdì”.
Idea risolutiva Si può supporre che esista un metodo in grado di convertire i tre parametri di input in un numero. Questo numero potrebbe, ad esempio, indicare i giorni trascorsi da una data prestabilita, chiamata data di riferimento, generalmente il primo giorno di un anno (è quindi un anno di riferimento). Sapendo il giorno settimanale della data di riferimento, il problema si risolverebbe calcolando il resto intero della divisione con 7 (numero di giorni in una settimana) e valutando correttamente il risultato ottenuto.
Origine del Calendario Gregoriano Il calendario gregoriano deve il suo nome al papa Gregorio XIII. Fino al 1582 era in vigore un calendario elaborato ai tempi di Giulio Cesare (calendario giuliano): questo era basato sull’idea, sbagliata, che un anno durasse esattamente giorni. Pertanto ogni 4 anni era inserito un anno bisestile che aveva un giorno in più a febbraio. In realtà si scoprì che la durata effettiva di un anno era di giorni, una differenza apparentemente insignificante (11 minuti di errore ogni anno), ma che nel 1582 aveva portato ad uno sfasamento della Pasqua di quasi due settimane!
La Riforma Gregoriana Fu così che il papa decise che il giorno seguente a giovedì 4 ottobre fosse venerdì 15 ottobre. Così facendo, era riuscito a risistemare i conteggi. Per risolvere il problema, stabilì inoltre di abolire 3 anni bisestili ogni quattrocento. In particolare, non erano considerati bisestili gli anni di inizio secolo (divisibili per 100), ma si includevano quelli divisibili per 400. Così gli anni 1700, 1800 e 1900 non sono bisestili, mentre lo è il Quindi sono bisestili gli anni divisibili per 4, escludendo quelli divisibili per 100 ma includendo quelli divisibili per 400. Questa regola è valida solo dopo l’anno 1582, prima un anno ogni 4 è bisestile.
I giorni dei mesi Un altro fattore da tenere a mente nella creazione di un calendario è la lunghezza (in giorni) dei mesi dell’anno. Gennaio 31 Luglio 31 Febbraio 28/29 Agosto 31 Marzo 31 Settembre 30 Aprile 30 Ottobre 31 Maggio 31 Novembre 30 Giugno 30 Dicembre 31
Scelta del progetto E’ importante scegliere come lavorare ad un progetto. Nel nostro caso, stabiliamo di curarci di date superiori all’anno 1582, per evitare dei problemi relativi allo stesso anno 1582, che risulta avere un numero di giorni nettamente inferiore agli altri per la riforma attuata dal papa. Del resto, difficilmente qualcuno sarà interessato ad una data del 14° secolo o anche precedente.
La funzione isBis() Per iniziare, si può scrivere una funzione che, ricevuto un anno, restituisce 1 se questo è bisestile, 0 se non lo è, 2 se l’anno è il 1582 (di cui è difficile dire con certezza che sia bisestile). short isBis (int anno) { if (anno <= 1582) if (anno < 1582) if (anno % 4) return 0; else return 1;
La funzione isBis() else // E’ il 1582 return 2; else { // Anno successivo alla riforma if (anno % 4) return 0; else if (anno % 100 == 0) if (anno % 400 == 0) return 1; else return 0; else return 1; }
Controllare l’input In un programma come questo, è fondamentale che l’input fornito dall’utente sia corretto, in quanto il programma eseguirebbe tranquillamente tutte le operazioni, generando tuttavia risultati totalmente errati. Saranno scritte le funzioni readYear(), readMonth() e readDay() che, utilizzando un ciclo do…while, realizzeranno l’input controllato. Per quanto riguarda la lettura del giorno, si assume come valore massimo accettabile il 31. Sarà costruita in seguito una funzione per il controllo globale della data. Inoltre, gli anni dovranno essere maggiori di 1582.
Verificare una data Letti i tre parametri di input, potrebbe succedere che una data specificata, come il 31 febbraio, lo stesso 29 febbraio per un anno non bisestile e così via, non sia corretta. E’ necessaria quindi una funzione checkDate() che svolga questo compito. Si può stabilire in partenza che sia verificata una data dando per scontato che siano state usate le tre funzioni prima definite, ed evitando quindi alcuni controlli (ad esempio sui numeri <= 0).
La funzione checkDate() short checkDate(int gg, int mm, int aa) { if (mm == 2) { if (isBis (aa)) if (gg > 29) return 0; else return 1; else if (gg > 28) return 0; else return 1; }
La funzione checkDate() else if (mm==4 || mm==6 || mm==9 || mm==11) if (gg > 30) return 0; else return 1; else if (gg > 31) return 0; else return 1; }
Lettura globale della data Le quattro funzioni scritte potrebbero tranquillamente interagire tra loro nel main(), ma questo porterebbe alla scrittura di un codice difficilmente riutilizzabile. Verrà invece scritta una funzione readDate(), in grado di leggere e automaticamente controllare tutti gli elementi di una data. Visto che i valori da restituire sono tre (giorno, mese, anno) le tre variabili corrispondenti saranno passate per indirizzo. L’input sarà fatto per mezzo delle funzioni readDay(), readMonth() e readYear(), mentre il controllo sarà fatto grazie a checkDate().
La funzione readDate() void readDate(int *gg, int *mm, int *aa) { // Lettura globale di una data int result; do { *gg = readDay(); *mm = readMonth(); *aa = readYear(); result = checkDate (*gg,*mm,*aa); if (!result) cout << "Data inesistente. Riprovare\n"; } while (!result); return; }
Calcolare il numero di giorni Arrivati a questo punto è necessario riuscire a calcolare il numero di giorni trascorsi dalla data di riferimento, da noi assunta come 1 gennaio 0. Per prima cosa scriveremo una funzione semplificata di isBis(), chiamata appunto isBisS() (isBisSemplificata). Questa, eliminando il problema di date inferiori o uguali al 1582, utilizza la regola del calendario gregoriano per verificare se un anno è bisestile. In seguito scriveremo due funzioni, complementari tra loro, date2num() e num2date() che trasformeranno una data in numero di giorni dal riferimento e viceversa.
La funzione isBisS() short isBisS (int anno) { // Versione semplificata di isBis() if (anno % 4 || anno == 0) return 0; else if (anno % 100 == 0) if (anno % 400 == 0) return 1; else return 0; else return 1; }
Dalla data al numero Passare dalla data al numero di giorni trascorsi dal riferimento risulta abbastanza semplice. Si tratterà infatti di incrementare opportunamente un contatore per tutti i mesi e gli anni precedenti a quelli forniti in input e di aggiungere quindi il giorno del mese. Così per il 7 febbraio 2005, aggiungeremo 2004 volte il numero di giorni di un anno (365 o 366 a seconda che l’anno analizzato sia bisestile o meno) e il numero di giorni di gennaio (31). Ovviamente non metteremo anche tutti quelli di febbraio, ma solo la parte fino ad oggi trascorsa (indicata dal giorno del mese). A questo punto, la funzione date2num() avrà questa definizione:
La funzione date2num() long date2num (int gg, int mm, int aa) { long ng = gg; for (int anno = 0; anno < aa; anno++) { if (isBisS(anno)) ng += 366; else ng += 365; }
La funzione date2num() for (int mese = 1; mese < mm; mese++) { if (mese == 2) if (isBisS(aa)) ng += 29; else ng += 28; else if (mese == 4 || mese == 6 || mese == 9 || mese == 11) ng += 30; else ng += 31; } return ng; }
Il problema inverso A questo punto si pone il problema inverso: ricevuto un solo input, ossia il numero di giorni trascorsi dall’anno 0, si devono ottenere i tre output corrispondenti alla data. Avendo tre variabili di output, anche questa volta sarà necessario passarle per indirizzo alla funzione. La funzione num2date() ha questo prototipo: void num2date (long ng, int *gg, int *mm, int *aa) All’interno si devono prevedere tutti i casi possibili per la trasformazione.
Il problema inverso La cosa che è risultata fondamentale nella scrittura di questa funzione è la necessità di avere la totale coerenza con la funzione inversa e complementare, ossia date2num(). Così, gli anni bisestili vengono analizzati anche qui con isBisS() e non con la versione completa della funzione. Teoricamente, infatti, una espressione simile a date2num(num2date(numero)) dovrebbe restituire il valore di partenza. Se così non fosse, è evidente che tra le due funzioni non c’è sufficiente coerenza.
Principio di risoluzione Il principio di risoluzione è quello di partire da un numero, sottrarre continuamente 365 o 366 (a seconda che l’anno in esame sia bisestile o meno), fino ad ottenere un numero che non consente ulteriori sottrazioni. A questo punto, partendo da gennaio, si sottraggono i giorni sufficienti per un mese, finché il numero non è più sufficiente per il mese successivo (ad esempio se si arriva a 23 e si deve analizzare marzo, è evidente che non è possibile farlo).
Determinare l’anno Questo ciclo, contenente una lunga condizione, permette in modo semplice ed immediato di determinare l’anno corrente, in quanto continua a sottrarre 365 giorni (o 366) fino a che non si ottiene un numero al quale non è più possibile sottrarre questo valore (e quindi un numero < 365) for (*aa = 0; ((ng >= 365 && (!(isBisS(*aa))))||(ng >= 366 && isBisS(*aa))); (*aa)++) if (isBisS(*aa)) ng -= 366; else ng -= 365;
Osservazione In questo caso il ciclo si basa sull’anno di riferimento 0. Nel caso si decidesse in seguito di cambiare il riferimento (ad esempio per lavorare con cifre più basse), basta sostituire l’inizializzazione del ciclo for con l’anno desiderato.
Primo Problema Provando lo spezzone di codice, effettivamente si nota che esso restituisce l’anno giusto in tutti i casi, tranne in uno: il 31 dicembre di un anno. Osservando il codice, si nota infatti che questo valore si ottiene quando il numero diventa esattamente 0. In questo caso il programma sballa di un anno. La correzione è abbastanza semplice e va posta immediatamente sotto il ciclo precedente. if (ng == 0) { *mm = 12; *gg = 31; (*aa) --; return; }
Determinare il mese Per determinare il mese si sarebbe potuto procedere in modo analogo all’anno, ma la condizione sarebbe diventata esageratamente complessa e avrebbe reso il codice totalmente illeggibile. Pertanto ho deciso di spezzettare l’analisi di tutti i mesi, nonostante questo porti a codice spesso molto simile e decisamente più lungo. Lo schema prevede innanzitutto l’inizializzazione del mese a 1, quindi l’analisi interna alla funzione diventa abbastanza semplice. Se possibile, si sottraggono i giorni del mese e si incrementa questa variabile, in caso contrario si esce dalla funzione.
Esempio: gennaio Ecco l’esempio del codice di gennaio: // Gennaio if (ng > 31) { (*mm)++; ng -= 31; } else { *gg = ng; return; }
Esempio: gennaio Se il numero attuale di giorni è maggiore del numero di giorni del mese in esame (in questo caso 31, visto che si analizza gennaio), si sottrae questa cifra e si incrementa il mese (in pratica si dice al programma che quel mese è trascorso totalmente e va quindi conteggiato). In caso contrario si assegna il numero restante di giorni al giorno del mese e si esce dalla funzione. In questo caso la prevenzione del numero di giorni uguale a 0 non è necessaria, visto che la condizione di ogni mese fa in modo che non si entri nel blocco se il numero di giorni è uguale al massimo (in pratica resterà sempre almeno 1).
Caso particolare: Febbraio Mentre per tutti gli altri mesi si può usare lo stesso identico codice di gennaio (modificando il valore massimo di giorni), per febbraio serve un codice particolare, per prevedere gli anni bisestili. Come detto, si userà isBisS() per coerenza con date2num() if ((ng > 29 && isBisS(*aa))||(ng > 28 && !isBisS(*aa))) { (*mm)++; if (isBisS(*aa)) ng -= 29; else ng -= 28; }
Caso particolare: Febbraio else { *gg = ng; return; } In sostanza il codice è rimasto lo stesso, solo che aggiunge la prevenzione per l’anno bisestile. Completata l’analisi dei 12 mesi dell’anno, il codice è finito e si può scrivere l’istruzione return finale.
Osservazione: blocco inutile Facendo un piccolo ragionamento, sono arrivato alla conclusione che il blocco finale, quello sull’analisi di dicembre, è del tutto superfluo, perché non ci saranno mai abbastanza giorni per entrarvi (se così fosse, sarebbe passato un anno intero e quindi ci sarebbe un errore nel ciclo di analisi dell’anno). Pertanto si può eliminare questo blocco e sostituirlo con *gg = ng; return; (esattamente il blocco else degli altri mesi).
Giorno della settimana Avendo un numero di giorni qualunque trascorsi da un anno di riferimento una divisione per 7 ci direbbe esattamente quante settimane sono trascorse da quella data. Se invece calcoliamo il resto di questa divisione dovremmo ottenere quanti giorni sono trascorsi dall’inizio dell’ultima settimana. Si può supporre quindi che calcolando il resto della divisione per 7 si ottenga esattamente il giorno della settimana. Resta solo da stabilire come interpretare questo valore (che ovviamente sarà compreso tra 0 e 6).
Interpretare il risultato Per stabilire come interpretare il risultato è necessario conoscere almeno il giorno della settimana di una qualsiasi data. Ad esempio, abbiamo detto per certo che il 17 dicembre 2004 era un Venerdì. Se inseriamo nel programma e calcoliamo il resto della divisione per 7 otteniamo il valore 6. Pertanto 6 è il valore che indica il Venerdì. Tutti gli altri giorni saranno riferiti a questo. 0 => Sabato 1 => Domenica 2 => Lunedì3 => Martedì 4 => Mercoledì5 => Giovedì 6 => Venerdì
Secondo problema: aggiungere Arriva ora un secondo problema: ricevuti in input una data e il numero di giorni da sommargli, stampare la nuova data. Si può risolvere questo problema convertendo la data originale in numero di giorni, sommare il secondo valore e convertire questo nuovo numero in una data. Per fare ciò scriveremo la funzione date_add(). In realtà, volendo aggiungere un numero negativo di giorni, otterremo il risultato di sottrarre questi giorni. Dovendo restituire anche in questo caso 3 valori, le variabili dovranno essere passate per indirizzo. Avremo quindi ben 7 parametri.
La funzione date_add() La funzione, pur avendo 7 parametri, ha uno schema abbastanza semplice. void date_add (int qta, int g_or, int m_or, int a_or, int *g_new, int *m_new, int *a_new) { long tot = date2num (g_or, m_or, a_or); tot += (long)qta; num2date (tot, &*g_new, &*m_new, &*a_new); return; }
Problema: Long e Int Un problema riscontrato nella compilazione del programma utilizzando TurboC è stato il fatto che questo non converte in automatico un tipo int in long. Pertanto tutti i valori utilizzati nelle funzioni date2num() e num2date() sono stati seguiti dalla lettera L ( 31L, 29L ecc) per forzare la conversione a long. Non eseguendo questo passaggio tutti i valori venivano completamente sballati dal programma.
Funzioni di supporto Per svolgere il meno lavoro possibile nel main() si possono scrivere anche altre funzioni di supporto: numDaysMonth() restituisce il numero di giorni di un mese specificato (va specificato anche se l’anno è bisestile o meno) coutDay() stampa in lettere il giorno della settimana ricevuto il valore numerico (0 => Sabato) coutMonth() stampa il lettere il nome di un mese ricevuto come numero (1 => Gennaio)
Stampare il calendario A questo punto è necessario arrivare a stampare il calendario di un mese ed anno specificato. Il vero problema riscontrato in questo caso è la necessità di “impaginare” i dati in colonne e righe. Il primo punto è quello che la numerazione dei giorni partendo da sabato è utile per problemi non visibili agli utenti, ma ben poco per quelli visibili: nella visualizzazione la prima colonna dovrà infatti essere il lunedì (come da tradizione per il calendario italiano). Pertanto la prima cosa da fare è convertire il primo giorno del mese in un numero “corretto” (lo 0 deve corrispondere al lunedì).
Conversione del giorno Per convertire il giorno in modo che lo zero sia il lunedì è sufficiente questo codice: if (gs >= 2) gs -= 2; else gs += 5; Dove gs contiene il valore numerico del giorno della settimana.
Problema della stampa Dovremmo stampare un numero crescente di valori da 1 fino a numDaysMonth (meseinesame, isBis(annoinesame). Ogni riga della tabella deve contenere 7 valori, ossia la divisione in settimane. Pertanto ci sarà un ciclo che viene eseguito 7 volte che stampa un numero. Questo ciclo sarà ripetuto nuovamente fino a quando ci saranno giorni disponibili per il mese in esame. Decidiamo di separare questi sette valori per riga attraverso la sequenza di tabulazione \t. A questo punto il vero problema è che non tutti i mesi iniziano di lunedì, quindi la prima settimana il ciclo dovrà essere eseguito un numero variabile di volte.
La prima settimana Se supponiamo che esame sia una variabile di supporto per la stampa di ogni settimana (valori quindi da 0 a 6), possiamo correggere in questo modo il ciclo per la prima settimana if (giorno == 1 && gs > 0) { for (int x = 0; x < gs; x++) cout << "\t"; esame += gs; } Così facendo ci siamo spostati del giusto numero di \t necessari ad arrivare alla giusta colonna.
Ultima settimana Ovviamente porta un problema anche il fatto che non tutti i mesi terminano di domenica. Sarà quindi necessario prevedere un’uscita anticipata dal ciclo (e conseguentemente dalla funzione) quando si è stampato l’ultimo giorno del mese. if (giorno == numDaysMonth(mm,isBis(aa))) { return; } Per il resto si tratterà di stampare il giorno in esame seguito dal carattere di tabulazione. Infine resta da sistemare il piccolo problema della grafica del calendario, che non può essere confusionata.
La funzione printCal() Raccogliendo tutte le informazioni e aggiungendo grafica al calendario, la funzione diventa così: void printCal (int mm, int aa) { int decoro; int num = numDaysMonth (mm, isBis(aa)); for (decoro = 0; decoro < 80; decoro ++) cout << "-"; coutMonth (mm); cout << " " << aa << endl; for (decoro = 0; decoro < 80; decoro ++) cout << "-";
La funzione printCal() cout << "| Lun\t| Mar\t| Mer\t| Gio\t| Ven\t| Sab\t| Dom\t|\n"; for (decoro = 0; decoro < 80; decoro ++) cout << "-"; int giorno = 1;
La funzione printCal() do { for (int esame = 0; esame < 7; esame++) { int gs = foundDay (giorno,mm,aa); // Lo zero è lunedì if (gs >= 2) gs -= 2; else gs += 5; if (giorno == 1 && gs > 0) { for (int x = 0; x < gs; x++) cout << "\t"; esame += gs; }
La funzione printCal() cout << "| " << giorno << "\t"; if (giorno == numDaysMonth(mm,isBis(aa))) { cout << "|\n"; for (int decoro = 0; decoro < 80; decoro ++) cout << "-"; return; } giorno++; }
La funzione printCal() cout << "|" << endl; for (int decoro = 0; decoro < 80; decoro ++) cout << "-"; } while (giorno <= num); for (decoro = 0; decoro < 80; decoro ++) cout << "*"; return; }
Il main() Finita la scrittura delle funzioni, si può passare al corpo principale del programma, che è costituito essenzialmente dal menù e dai vari blocchi. Ho scelto di inizializzare le variabili contenenti le risposte (per rieseguire un blocco o tutto il programma) al valore ‘n’, in modo che si possa sapere quando ci si trova nel primo ciclo (il programma non entra mai nel ciclo do…while con risposta uguale a ‘n’ se non alla prima iterazione).
Il blocco del calendario All’interno del blocco del calendario si può aggiungere una piccola finezza: la possibilità di scorrere avanti e indietro con le frecce sinistra e destra i mesi dell’anno, in modo da poter vedere molto facilmente tutti i mesi. Per far questo è necessario rilevare la pressione dei tasti e volendo di CANC per uscire dal ciclo. In C++ questo si ottiene con due getch(), di cui solo il secondo viene utilizzato. Premendo la freccia sinistra il secondo getch() restituisce 75, premendo destra 77, CANC 83.
Muoversi nel calendario Il blocco che ci interessa è così formato: do { printCal (m,a); do { char supp = getch(); risp3 = getch(); } while ((int)risp3 != 75 && (int)risp3 != 77 && (int)risp3 != 83); // Modifica di m e a (prossimi fotogrammi) } while ((int)risp3 != 83);
Mese precedente if ((int)risp3 == 75) { // Mese precedente if (m == 1) { m = 12; a--; } else { m--; }
Mese successivo if ((int)risp3 == 77) { // Mese successivo if (m == 12) { m = 1; a++; } else { m++; }
Spiegazione del codice Il codice del mese precedente prevede di base che venga decrementato il valore del mese; tuttavia va preveduto il fatto che prima del mese gennaio (1) c’è il mese Dicembre (12) dell’anno prima. Ragionamento analogo e inverso va fatto per il mese successivo (dopo il mese 12 c’è il mese 1 dell’anno successivo). Alla prima entrata nel ciclo i valori di mese e anno sono ricavati dall’input dell’utente, mentre alle ripetizioni successive questi valori derivano da modifiche apportate all’entrata precedente nel ciclo (e quindi dalla pressione delle frecce).
Riepilogo Alla fine il programma sembra funzionare correttamente. Al momento non sono implementate le opzioni 4 e 5, in quanto non sono riuscito a trovare il modo di ricavare la data di sistema attuale in un formato a me utile. Il problema maggiore, effettivamente, è stato realizzare la funzione num2date(), soprattutto perché all’inizio non tenevo conto dell’importanza di avere una perfetta coerenza con la funzione opposta, date2num(). Risolti questi problemi, gli altri si sono rivelati di difficoltà nettamente inferiore.
Prototipi delle funzioni short isBis (int); short isBisS (int); int readYear (); int readMonth (); int readDay (); short checkDate (int,int,int); void readDate (int*,int*,int*); int foundDay (int,int,int); void num2date (long,int*,int*,int*); long date2num (int,int,int); void date_add (int,int,int,int,int*,int*,int*); int numDaysMonth (int,int); void coutDay (int,int,int); void coutMonth (int); void printCal (int,int);