Today I had to find a way to perform some rather "uncommon" DateTime-based operations: to be more specific, I needed to check if a date object resulting from a user input was at least two business days ahead of the current day.
I initially thought that excluding the Saturday and Sunday weekdays would have been enough, however I soon realized that I had underestimated the problem: I halso had to put the holidays into the loop. As if it wasn't enough, I also had to consider the fact that each country - and even each town, if we consider the italian's patron days - has their own holidays. Therefore, I had to find a way to programmatically handle various sets of possible holidays, such as:
- country-invariant holidays (at least for the Western countries - such as January, 01)
- calculated holidays (such as Easter and Easter monday).
- country-specific holidays (such as the Italian liberation day or the United States ID4).
- town-specific holidays (such as the Rome St. Patron Day).
- any other custom-made holiday (such as "tomorrow our office wil be closed").
Eventually, I came out with the following set of helper/extensions classes: although they aren't blatantly elegant, as they do make a massive use of unefficient loops, they are decent enough to solve these kind of issues for good. I'm dropping the whole source code here in this post, hoping it will be useful to someone else as well.
Source Code
Here's the source code: notice that main methods (AddBusinessDays, SubtractBusinessDays and GetBusinessDays) can be either used as static helper methods or extension methods.
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; } } |
Usage info
The code is quite self-explanatory, however here's a couple examples to explain how you can use it.
Add 10 business days (skipping only saturday and sunday week days)
1 |
var dtResult = DateTimeUtil.AddBusinessDays(srcDate, 10); |
Add 10 business days (skipping saturday, sunday and all country-invariant holidays for 2019)
1 |
var dtResult = DateTimeUtil.AddBusinessDays(srcDate, 10, GetHolidays(2019)); |
Add 10 business days (skipping saturday, sunday and all italian holidays for 2019)
1 |
var dtResult = DateTimeUtil.AddBusinessDays(srcDate, 10, GetHolidays(2019, "IT")); |
Add 10 business days (skipping saturday, sunday, all italian holidays and the Rome-specific holidays for 2019)
1 |
var dtResult = DateTimeUtil.AddBusinessDays(srcDate, 10, GetHolidays(2019, "IT", "Rome")); |
Conclusion
That's pretty much it: if you like the source code and/or if you need additional info, feel free to comment. Happy coding!
Hi your code is beautiful.
May I know how to use is this. I mean How Am I going to put this in button
Thank you,
In button? What do you mean?
Hi,
When I use an error, it says that the value cannot be converted when entering the year within the GetHolidays (year) method, debugging to understand what happens.
Erro:
Argument 1: cannot convert from ‘int’ to ‘System.Collections.Generic.IEnumerable’ [ColletionsAdvanced]csharp(CS1503)
IList intYear = new List();
intYear.Add(2022);
intYear.Add(2023);