L'articolo di oggi nasce da un'esigenza particolare che ho avuto oggi in ufficio, legata a una serie di operazioni particolari da effettuare con le date: in particolare, avevo la necessità di confrontare un oggetto DateTime ricavato da una data inserita da un utente con data odierna, dopo aver incrementato quest'ultima di due giorni lavorativi.
Inizialmente ho pensato che, per risolvere il problema, sarebbe stato sufficiente escludere i sabati e le domeniche: tuttavia, mi sono ben presto reso conto di aver sottovalutato il problema, in quanto una simile soluzione non prende in considerazione una serie di festività nazionali, internazionali e addirittura cittadine (ad es. le feste dei patroni di Roma, Milano etc.). In buona sostanza, oltre a tutti i sabati e le domeniche dovevo gestire anche:
- Le festività internazionali (o perlomeno quelle valide in occidente, come ad es. natale e capodanno).
- Le festività calcolate (Pasqua e Pasquetta).
- Le festività nazionali (come ad esempio il 25 aprile in Italia o il 4 luglio negli Stati Uniti d'America).
- Le festività cittadine (come S. Pietro e Paolo a Roma, che cade il 29 giugno, o S. Ambrogio a Milano, che cade il 7 dicembre).
- Qualsiasi altra festività o altro giorno non lavorativo personalizzato (ad es. i giorni di chiusura aziendale).
Tutti questi ragionamenti mi hanno portato a sviluppare una serie di metodi helper che, pur se non eccezionalmente eleganti, consentono di gestire in modo efficace tutte queste casistiche. Ho deciso di pubblicare il codice sorgente relativo all'attività all'interno di questo articolo, nella speranza che possano tornare utili anche a qualche altro sviluppatore.
Codice Sorgente
Come si può vedere, i metodi principali (AddBusinessDays, SubtractBusinessDays and GetBusinessDays) possono essere usati come normali metodi helper statici o come extension method.
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
/// <summary> /// Helper/extension class for manipulating date and time values. /// </summary> public static class DateTimeExtensions { /// <summary> /// Adds the given number of business days to the <see cref="DateTime"/>. /// </summary> /// <param name="current">The date to be changed.</param> /// <param name="days">Number of business days to be added.</param> /// <param name="holidays">An optional list of holiday (non-business) days to consider.</param> /// <returns>A <see cref="DateTime"/> increased by a given number of business days.</returns> public static DateTime AddBusinessDays( this DateTime current, int days, IEnumerable<DateTime> holidays = null) { var sign = Math.Sign(days); var unsignedDays = Math.Abs(days); for (var i = 0; i < unsignedDays; i++) { do { current = current.AddDays(sign); } while (current.DayOfWeek == DayOfWeek.Saturday || current.DayOfWeek == DayOfWeek.Sunday || (holidays != null && holidays.Contains(current.Date)) ); } return current; } /// <summary> /// Subtracts the given number of business days to the <see cref="DateTime"/>. /// </summary> /// <param name="current">The date to be changed.</param> /// <param name="days">Number of business days to be subtracted.</param> /// <param name="holidays">An optional list of holiday (non-business) days to consider.</param> /// <returns>A <see cref="DateTime"/> increased by a given number of business days.</returns> public static DateTime SubtractBusinessDays( this DateTime current, int days, IEnumerable<DateTime> holidays) { return AddBusinessDays(current, -days, holidays); } /// <summary> /// Retrieves the number of business days from two dates /// </summary> /// <param name="startDate">The inclusive start date</param> /// <param name="endDate">The inclusive end date</param> /// <param name="holidays">An optional list of holiday (non-business) days to consider.</param> /// <returns></returns> public static int GetBusinessDays( this DateTime startDate, DateTime endDate, IEnumerable<DateTime> holidays) { if (startDate > endDate) throw new NotSupportedException("ERROR: [startDate] cannot be greater than [endDate]."); int cnt = 0; for (var current = startDate; current < endDate; current = current.AddDays(1)) { if (current.DayOfWeek == DayOfWeek.Saturday || current.DayOfWeek == DayOfWeek.Sunday || (holidays != null && holidays.Contains(current.Date)) ) { // skip holiday } else cnt++; } return cnt; } /// <summary> /// Calculate Easter Sunday for any given year. /// src.: https://stackoverflow.com/a/2510411/1233379 /// </summary> /// <param name="year">The year to calcolate Easter against.</param> /// <returns>a DateTime object containing the Easter month and day for the given year</returns> public static DateTime GetEasterSunday(int year) { int day = 0; int month = 0; int g = year % 19; int c = year / 100; int h = (c - (int)(c / 4) - (int)((8 * c + 13) / 25) + 19 * g + 15) % 30; int i = h - (int)(h / 28) * (1 - (int)(h / 28) * (int)(29 / (h + 1)) * (int)((21 - g) / 11)); day = i - ((year + (int)(year / 4) + i + 2 - c + (int)(c / 4)) % 7) + 28; month = 3; if (day > 31) { month++; day -= 31; } return new DateTime(year, month, day); } /// <summary> /// Retrieve holidays for given years /// </summary> /// <param name="years">an array of years to retrieve the holidays</param> /// <param name="countryCode">a country two letter ISO (ex.: "IT") to add the holidays specific for that country</param> /// <param name="cityName">a city name to add the holidays specific for that city</param> /// <returns></returns> public static IEnumerable<DateTime> GetHolidays(IEnumerable<int> years, string countryCode = null, string cityName = null) { var lst = new List<DateTime>(); foreach (var year in years.Distinct()) { lst.AddRange(new[] { new DateTime(year, 1, 1), // 1 gennaio (capodanno) new DateTime(year, 1, 6), // 6 gennaio (epifania) new DateTime(year, 5, 1), // 1 maggio (lavoro) new DateTime(year, 8, 15), // 15 agosto (ferragosto) new DateTime(year, 11, 1), // 1 novembre (ognissanti) new DateTime(year, 12, 8), // 8 dicembre (immacolata concezione) new DateTime(year, 12, 25), // 25 dicembre (natale) new DateTime(year, 12, 26) // 26 dicembre (s. stefano) }); // add easter sunday (pasqua) and monday (pasquetta) var easterDate = GetEasterSunday(year); lst.Add(easterDate); lst.Add(easterDate.AddDays(1)); // country-specific holidays if (!String.IsNullOrEmpty(countryCode)) { switch (countryCode.ToUpper()) { case "IT": lst.Add(new DateTime(year, 4, 25)); // 25 aprile (liberazione) break; case "US": lst.Add(new DateTime(year, 7, 4)); // 4 luglio (Independence Day) break; // todo: add other countries default: // unsupported country: do nothing break; } } // city-specific holidays if (!String.IsNullOrEmpty(cityName)) { switch (cityName) { case "Rome": case "Roma": lst.Add(new DateTime(year, 6, 29)); // 29 giugno (s. pietro e paolo) break; case "Milano": case "Milan": lst.Add(new DateTime(year, 12, 7)); // 7 dicembre (s. ambrogio) break; // todo: add other cities default: // unsupported city: do nothing break; } } } return lst; } } |
Esempi di utilizzo
Il codice sorgente dovrebbe essere abbastanza chiaro: ad ogni buon conto, ecco una serie di esempi che spiegano come è possibile utilizzarlo.
Aggiungere 10 giorni lavorativi (saltando solo i sabati e le domeniche)
1 |
var dtResult = DateTimeUtil.AddBusinessDays(srcDate, 10); |
Aggiungere 10 giorni lavorativi (saltando i sabati, le domeniche e le feste internazionali previste per il 2019)
1 |
var dtResult = DateTimeUtil.AddBusinessDays(srcDate, 10, GetHolidays(2019)); |
Aggiungere 10 giorni lavorativi (saltando i sabati, le domeniche e le feste internazionali e nazionali italiane previste per il 2019)
1 |
var dtResult = DateTimeUtil.AddBusinessDays(srcDate, 10, GetHolidays(2019, "IT")); |
Aggiungere 10 giorni lavorativi (saltando i sabati, le domeniche, le feste internazionali, nazionali italiane e della città di Roma previste per il 2019)
1 |
var dtResult = DateTimeUtil.AddBusinessDays(srcDate, 10, GetHolidays(2019, "IT", "Rome")); |
Conclusione
Per il momento è tutto: qualora queste classi vi siano utili, o se avete bisogno di ulteriori informazioni, sentitevi liberi di aggiungere il vostro commento in fondo a questo articolo. Alla prossima, e... felice sviluppo!
nice job, ma la GetBusinessDays looppa
Hai ragione, avevo scritto male il codice nel post: ora dovrebbe essere ok.
new DateTime(year, 6, 1) rappresenta il 1° giugno, non il 1° maggio
Corretto, grazie :)
se si passano due anni uguali, la GetHolidays() restituisce il doppio dei valori
Ho aggiunto un Distinct() per evitare questa possibilità