Introduction
While writing code for the Home Theater PC software I'm developing, I realized a need to be able to determine the date of the Nth day of a given month for the purpose of determining when some holidays occur. For example, Thanksgiving (in the US) is celebrated on the fourth Thursday of November. This requirement led me to develop the following extension methods for the DateTime
class.
The Code
First, I needed to establish the necessary list of parameters. To make it as generic as possible at its most basic level, I needed to know the year, month, and day of week we're looking for, as well as the instance (ordinal
) of that day of the week within the specified month.
public static DateTime GetDateByOrdinalDay(this DateTime dt,
int year,
int month,
DayOfWeek day,
int ordinal)
{
Next, I needed to perform some sanity checks to make sure the programmer couldn't do something completely stupid (not that we programmers do that a lot). I also wanted the code to adjust appropriately for ordinals that were outside the allowable range. If an ordinal less than 1 is specified, the code self-corrects and assigns 1 to the variable. If the value is greater than 5, it makes the value 5. There are no months that have more than five of any given weekday, so I figure this is a reasonable move. If you want to interrupt the code, you could always modify it to throw an exception instead of correcting itself.
ordinal = Math.Min(Math.Max(ordinal, 1), 5);
month = Math.Min(Math.Max(month, 1), 12);
After that, I create a DateTime
object that I am free to thrash on (using the 1st day of the year/month), as well as determining the maximum number of days we can add to build the desired date. Since we're looking for the Nth instance of a given weekday in a given month, it doesn't make sense to return a date that falls outside that month. This value will help us later in the method.
DateTime workingDate = new DateTime(year, month, 1);
DateTime lastDay = new DateTime(year += (month + 1 > 12) ? 1 : 0,
month += (month + 1 > 12)? - 11 : 1,
1).AddDays(-1);
maxDays = (lastDay - workingDate).Days + 1;
Next, we need to determine how many days to add to the current working date (remember, it represents the first day of the specified month). To start, we need to calculate the gap between the first occurrence of the specified DayOfWeek
, and the first day of the month.
int gap = 0;
if (workingDate.DayOfWeek != dayOfWeek)
{
gap = (int)workingDate.DayOfWeek - (int)dayOfWeek;
gap = (gap < 0) ? Math.Abs(gap) : 7 - gap;
workingDate = workingDate.AddDays(gap);
}
At this point, our working date is set to the first instance (ordinal=1) of the specified DayOfWeek
, so all we have to do is add enough days to accommodate the specified ordinal value (and we only have to do it if the specified ordinal is greater than 1.
if (ordinal > 1)
{
int daysToAdd = 7 * (ordinal - 1);
while (daysToAdd + gap > maxDays-1)
{
daysToAdd -= 7;
}
workingDate = workingDate.AddDays(daysToAdd);
}
Finally, we return the calculated date.
Usage
Usage is simple:
rDateTime thanksgiving = DateTime.GetDateByOrdinalDay(DateTime.Now.Year,
11,
System.DayOfWeek.Thursday,
4);
Final Notes
The class included in the download includes an overloaded version of this method that assumes that the current year is to be used, as well as a Set...
method to allow the calling object to set itself to the result.
The original version of the code used recursion, which I replaced with a while...
loop. The reason for the sanity checks is that I try to avoid using exceptions if at all possible. Since there's no reason the code shouldn't be able to self-correct, that's the approach I took.
Have a ball.
History
12 Nov 2012 - Refactored the main method in the extension class because I found some issues under certain circumstances.
Aug 2012 - Original Article