Date date e vi saranno date date
a cura di Diego Cattaruzza (requisiti: nessuno)Premessa
Dopo aver sviluppato la Tip "Trovare il primo giorno (lunedì) di una settimana ", mi sono ricordato di avere da qualche parte un modulo per VBA di Access che contiene una serie di funzioni relative alle date, così ho pensato di sviluppare una libreria con le stesse funzioni in VB.Net.dcDateTimeLib
dcDateTimeLib è il Namespace cui appartiene la classe DateTimeTool
Caratteristica di questa libreria è che non deve esserci il bisogno di istanziare un oggetto della classe che espone le funzioni, che però devono essere ugualmente disponibili.
Ciò significa che tutti i metodi della classe devono essere Public Shared, e che il solo metodo costruttore deve essere Private.Private Sub New() ' End SubDateTimeTool espone i seguenti metodi:
Age restituisce l'età CountDowInMonth restituisce quante volte è presente, in un dato mese, un dato giorno di settimana (ad esempio, quante domeniche) CountHolidays restituisce il numero festività intercorrenti tra due date CountWorkdays restituisce il numero di giorni lavorativi intercorrenti tra due date EasterDate calcola la data della Pasqua di un dato anno FirstDayInMonth restituisce la data del primo giorno di un mese (è usata da altre funzioni) FirstDayInQuarter restituisce la data del primo giorno di un trimestre FirstDayInWeek restituisce la data del primo giorno di una settimana FirstWorkdayInMonth restituisce la data del primo giorno lavorativo in un mese IsHoliday verifica se una data corrisponde ad una festività IsWeekend verifica se una data cade di sabato o di domenica LastDayInMonth restituisce la data dell'ultimo giorno di un mese LastDayInQuarter restituisce la data dell'ultimo giorno di un trimestre LastDayInWeek restituisce la data dell'ultimo giorno di una settimana LastWorkdayInMonth restituisce la data dell'ultimo giorno lavorativo in un mese NextAnniversary restituisce la data della prossima ricorrenza dello stesso mese e giorno forniti NextDow restituisce la data del prossimo giorno di settimana NextWorkday restituisce la data del prossimo giorno lavorativo NthDow restituisce la data dell'ennesimo giorno di settimana a partire da una data, o in un mese PreviousDow restituisce la data del precedente giorno di settimana PreviousWorkday restituisce la data del precedente giorno lavorativo SkipHolidays restituisce la data del primo giorno prima o dopo un periodo festivo Optional e overload
In una prima versione, in quasi tutti i metodi, viene richiesta una data per farvi riferimento relativamente al periodo che si sta indagando (la settimana o il mese che la contiene, la data di partenza), ma questo parametro è opzionale, per dare modo di usare comodamente la data di default, che è ovviamente quella odierna.
Poiché in VB.Net, quando si impostano parametri opzionali, bisogna comunque specificare questo valore di default, ho assegnato il valore Nothing, e nel codice iniziale verificavo l'inizializzazione così:Public Shared Function Age(ByVal birthday As Date, Optional ByVal atDate As Date = Nothing) As Integer If atDate.Year = 1 Then atDate = Today Return (atDate.Subtract(birthday.AddDays(-1)).Days \ 365) End FunctionL'anno di una data Nothing è 1 (poiché non funzionava 'If IsNothing(atDate)...', ho verificato in fase di debug. Quindi, se l'anno della data opzionale è 1, allora imposto la data a quella odierna.
Questa impostazione si è rivelata errata, perché per Visual Studio 2003 Nothing è sempre e comunque un Object e non è possibile convertire il tipo di una costante. Non era possibile nemmeno assegnare il valore minimo del tipo (DateTime.MinValue).
Quindi ho convertito tutto il codice in modo da avere un overload di ciascun metodo che presentasse un parametro opzionale di tipo Datetime impostato a Nothing. Il primo metodo, privo di tale parametro, chiama il secondo metodo passando la data odierna. Naturalmente non c'è più la 'geniale' cretinata di verificare se l'anno della data è 1.
Il metodo Age di cui sopra diventa:Public Shared Function Age(ByVal birthday As DateTime) As Integer Return Age(birthday, DateTime.Today) End Function Public Shared Function Age(ByVal birthday As DateTime, ByVal atDate As DateTime) As Integer Return (atDate.Subtract(birthday.AddDays(-1)).Days \ 365) End FunctionIl metodo Age calcola l'età (o piuttosto gli anni trascorsi da birthday a atDate) sottraendo alla data posteriore (atDate) quella del giorno precedente la data anteriore (birthday), in modo da ottenere un periodo di tempo (oggetto TimeSpan), del quale ricavare facilmente il numero dei giorni (Days), che infine divido per 365 (i giorni di un anno), così che ne risulti un intero (l'operatore barra trasversa).
Questo è l'unico metodo di cui mostro entrambi gli overload, per tutti gli altri mi limito all'overload con la data.
Ho usato altri parametri opzionali, piuttosto che sviluppare omonime funzioni in overload, perché in alcuni casi mi serviva proprio fornire la comodità di omettere argomenti.
Ho sviluppato un solo metodo con un vero overload, ma in questo caso la firma è nettamente diversa.Public Shared Function FirstDayInWeek(ByVal data As DateTime) As DateTime Return data.AddDays(1 - data.DayOfWeek) End Function Public Shared Function FirstDayInWeek(ByVal week As Integer, Optional ByVal year As Integer = 0) _ As DateTime Dim dt As DateTime, d As Integer If year = 0 Then year = DateTime.Today.Year dt = New DateTime(year, 1, 1) d = dt.DayOfWeek If d = 0 Then d = 7 If d < DayOfWeek.Thursday Then dt = dt.AddDays(7) Else week -= 1 End If dt = FirstDayInWeek(dt) dt = dt.AddDays(7 * week) Return dt End FunctionNel primo caso è richiesta la data di un giorno compreso nella settimana in questione, nel secondo, invece, il numero della settimana e l'anno relativo (in altri termini, nome e codice migliorati, la Tip che ha dato origine a questo articolo).
Trovare il primo giorno di una settimana, avendo una data, è semplice: si sottrae alla data passata il numero di giorni corrispondente al giorno di settimana della data stessa e si aggiunge 1 per avere il primo giorno.
Sulla base del numero di una settimana e dell'anno, il calcolo è leggermente più complesso: si trova il giorno di settimana del primo dell'anno, per determinare poi la direzione in cui cercare il primo lunedì, da cui partire per ricavare la data cercata. Infatti, se il giorno di settimana del primo dell'anno è precedente al giovedì, la prima settimana dell'anno è quella che comincia con il lunedì precedente (o lo stesso, se il primo dell'anno è lunedì), con il lunedì successivo, in caso contrario. Nel primo caso, si sposta di una settimana la data di partenza, nel secondo si decrementa il numero di settimane. Benché sembri una contraddizione, non lo è se si tiene conto del fatto che tale data di partenza viene passata come argomento all'altro metodo FirstDayInWeek.
Altre eventuali sottigliezze dell'algoritmo le lascio scoprire a voi, mentre trovo molto più interessante farvi notare che in poche righe ho avuto la possibilità di usare parecchi potenti metodi esposti da varie classi di .Net (alla rinfusa: Today, Subtract, AddDays - della classe DateTime - Days della classe TimeSpan, l'enumerato DayOfWeek) e di imparare che DateTime indica un momento, mentre TimeSpan definisce un periodo di tempo.
Insomma, Intellisense, curiosità + intelligenza, portano ad una proficua consultazione dell'help, per rendersi conto almeno in piccola parte della potenza che ci viene messa a disposizione.Versatilità
Alcuni metodi sono stati studiati in modo da offrire una certa versatilità d'uso, come nel caso di NthDow:Public Shared Function NthDow(ByVal startDate As DateTime, ByVal n As Integer, _ ByVal dow As DayOfWeek, _ Optional ByVal fromMonthFirst As Boolean = True) As DateTime Dim dt As DateTime = startDate If fromMonthFirst Then dt = FirstDayInMonth(dt) For i As Integer = 1 To n dt = NextDow(dow, dt) Next Return dt End FunctionDi default, infatti, viene restituita la data dell'ennesimo giorno di settimana del mese (ad esempio, il terzo venerdì), ma, se il parametro fromMonthFirst viene impostato a False, il calcolo parte proprio dalla data fornita startDate (ad esempio, la dodicesima domenica a partire da...).
En passant, faccio notare che all'interno di alcuni metodi vengono ovviamente chiamati altri metodi della stessa libreria, tra cui qualcuno di cui apparentemente non si vedrebbe la necessità, come FirstDayInMonth (chi non sa trovare il primo giorno di un mese?): l'utilità sta, in questo caso, nella leggibilità del codice.Un altro metodo versatile è SkipHolidays, di cui parlerò nel paragrafo seguente.
Festività
Un discorso a parte è quello relativo alle festività. E' indubbio che la soluzione migliore per questo problema, tipico per ogni sviluppatore, sia quello di provvedere un elenco delle festività fisse specifico per ogni nazione (o, per dirla come in .Net, per ogni cultura), assieme a specifiche procedure per le festività 'dinamiche' (non solo la Pasqua, ma anche quelle feste che cadono in ennesimi giorni di settimana. Questo elenco può essere fornito sotto varie forme, ma comunque tramite file aggiunti (anche un file di risorse localizzato, un file xml, txt, ini, di database, e chi più ne ha, più ne metta).
E' la soluzione migliore perché permette, appunto, di generalizzare la procedura di verifica della festività di una qualsiasi data in una qualsiasi cultura.Io, però, ho preferito limitarmi alle festività italiane. Ecco quindi implementata la funzione IsHoliday:
Public Shared Function IsHoliday(ByVal data As DateTime) As Boolean Dim y As Integer = data.Year Dim m As Integer = data.Month Dim d As Integer = data.Day If m = 1 And d = 1 Or m = 1 And d = 6 Or m = 4 And d = 25 Or _ m = 5 And d = 1 Or m = 6 And d = 2 Or m = 8 And d = 15 Or _ m = 11 And d = 1 Or m = 12 And d = 8 Or m = 12 And d = 25 Or _ m = 12 And d = 26 Or data.Equals(EasterDate(y).AddDays(1)) Then Return True Else Return False End If End FunctionQuesta funzione ritorna vero (la data passata è una festività) se il mese e il giorno sono tra quelli elencati nella lunga condiizone della If, che si conclude con la verifica del Lunedì dell'Angelo, cioè la data ottenuta aggiungendo un giorno alla data della Pasqua, che viene calcolata dalla seguente funzione:
Public Shared Function EasterDate(Optional ByVal year As Integer = 0) As DateTime Static dt As DateTime Dim G, C, H, i, j, L As Integer Dim m, d As Integer If year = 0 Then year = DateTime.Today.Year If dt.Year <> year Then G = year Mod 19 C = year \ 100 H = ((C - (C \ 4) - ((8 * C + 13) \ 25) + (19 * G) + 15) Mod 30) i = H - ((H \ 28) * (1 - (H \ 28) * (29 \ (H + 1)) * ((21 - G) \ 11))) j = ((year + (year \ 4) + i + 2 - C + (C \ 4)) Mod 7) L = i - j m = 3 + ((L + 40) \ 44) d = L + 28 - (31 * (m \ 4)) dt = New DateTime(year, m, d) End If Return dt End FunctionE' la stessa funzione che si trova quasi dappertutto (questa, nello specifico, è la conversione in VB.Net da una scritta in VB6 da Alberto Falossi), con in più l'uso di una variabile statica per conservare l'ultima Pasqua calcolata ed evitare di rifare lo stesso calcolo per più date dello stesso anno.
Infatti, sia IsHoliday (e quindi EasterDate) che IsWeekend sono usate ciclicamente da SkipHolidays, che è il metodo per scalare (in avanti o all'indietro) le date successive o precedenti quella passata in argomento, alla ricerca del primo giorno non festivo.
Public Shared Function SkipHolidays(ByVal data As DateTime, _ Optional ByVal forward As Boolean = True) As DateTime Dim d As Integer = 1 If Not forward Then d = -1 Do While IsHoliday(data) Or IsWeekend(data) data = data.AddDays(d) Loop Return data End FunctionQuesto metodo, il cui algoritmo è superfluo illustrare, viene usato da tutti i metodi relativi ai giorni lavorativi o festivi (CountHolidays, CountWorkdays, FirstWorkdayInMonth, LastWorkdayInMonth, NextWorkday, PreviousWorkday).
Altri metodi
Sugli altri metodi non c'è molto da dire, tranne che può essere istruttivo analizzarne gli algoritmi per conto proprio (qui richiederebbe troppo spazio e d'altra parte si tratta di metodi abbastanza semplici).
Faccio solo notare come si sostituisce la DateSerial di Visual Basic (presente comunque nella libreria di VB.Net, ma che io cerco di non usare esplicitamente), quando si vuole ottenere l'ultimo giorno di un mese.
In VB6 si fa così:DateSerial(anno, mese + 1, 0)In VB.Net si fa così:
New DateTime(data.Year, data.Month, DateTime.DaysInMonth(data.Year, data.Month))Usare la libreria
E' semplice. Una volta compilata, la si include nelle References del progetto in cui voglliamo usarla e, nel modulo in cui serve, si inserisce la direttiva Imports relativa:Imports dcDateTimeLib.DateTimeToolEcco un esempio di codice di test:
Dim results As String = _ "giovedì scorso era il " & PreviousDow(DayOfWeek.Thursday).ToShortDateString & vbCrLf & _ "giovedì prossimo sarà il " & NextDow(DayOfWeek.Thursday).ToShortDateString & vbCrLf & _ "il terzo lunedì del mese è il " & NthDow(Today, 3, DayOfWeek.Monday) & vbCrLf & _ "in questo mese ci sono " & CountDowInMonth(DayOfWeek.Thursday) & " gioved\'ec" & vbCrLf & _ "questo mese va dal " & FirstDayInMonth.ToShortDateString & " al " & _ LastDayInMonth() & vbCrLf & _ "questa settimana va dal " & FirstDayInWeek.ToShortDateString & " al " & _ LastDayInWeek.ToShortDateString & vbCrLf & _ "il prossimo 25 aprile sarà il " & NextAnniversary(New DateTime(2006, 4, 25)) & vbCrLf & _ "il primo giorno della 40ma settimana dell'anno è il " & _ FirstDayInWeek(40).ToShortDateString & vbCrLf & _ "questo trimestre va dal " & FirstDayInQuarter.ToShortDateString & " al " & _ LastDayInQuarter.ToShortDateString & _ "quest'anno la Pasqua cade il " & EasterDate.ToShortDateString & vbCrLf & _ "il primo giorno lavorativo del mese è il " & FirstWorkdayInMonth.ToShortDateString & _ " e l'ultimo è il " & LastWorkdayInMonth.ToShortDateString & vbCrLf & _ "il prossimo giorno lavorativo è il " & NextWorkday.ToShortDateString & _ " e il precedente era il " & PreviousWorkday.ToShortDateString & vbCrLf & _ "in questo mese ci sono " & _ CountWorkdays(New DateTime(2006, 8, 1), New DateTime(2006, 8, 31)) & " giorni lavorativi e " & _ CountHolidays(New DateTime(2006, 8, 1), New DateTime(2006, 8, 31)) & " giorni festivi" MsgBox(results)Si può notare la comodità di non passare l'argomento opzionale.
Invito comunque a 'studiare' le cose nuove che si leggono nel codice, che sarebbe stato troppo dispersivo illustrare in questo articolo.Il codice ed i commenti
Tutto il codice è scaricabile dall'area download. In esso potrete notare i commenti di documentazione, che ho inserito facendo uso del ClassDiagram, nel quale non solo si creano le procedure con i loro parametri (ho dovuto aggiungere 'a mano' solo la parola Shared), ma si possono inserire le info di sommario. Queste info vengono presentate da Intellisense, quando si sceglie uno dei metodi di DateTimeTool.Approfitto della correzione dell'errore relativo al parametro opzionale, per fornire anche la versione in C#, preparata da Sabrina Cosolo (solo il file DateTimeTool.cs).
Conclusione
Spero di aver fornito qualcosa di utile. Ogni suggerimento su nuovi metodi, o migliorie da apportare, o critica o dubbio da esporre, relativamente a questo articolo, saranno graditi.