Introduction
This class returns all of the holidays that occur in a one-year period. The definition of each holiday is specified in an XML configuration file. This class can be used in any instance where you need to present a list of upcoming holidays for any specific date. Because all the holidays are defined in an XML file, the application can be customized for different cultures and specific different uses.
The problem
In our organization, we are building an application to help owners of small businesses manage their own marketing programs. One feature of this application is to remind the user of the upcoming holidays so that they can plan ahead for sales and promotions based on those dates. When we came up with this solution, our objective was to create a way to set up the holidays that we want the application to use, and not have to worry about it ever again. The difficulty of this is that many holidays fall on different dates on different years, and that the rules determining what dates they fall on are varied.
The solution
We decided that it would be ideal to store all of the rules for all of the different holidays in an XML file, and then write a class that could interpret those rules and calculate the correct date for each holiday. To do this we had to separate out the holidays into different types which were calculated similarly to each other. Below are the different types of holidays that we dealt with and a sample XML configuration for each:
- Those that occur on the same month and same day each year. (E.g. Groundhog day is always on February 2nd.)
<Holiday name="Groundhog Day">
<Month>2</Month>
<Day>2</Day>
</Holiday>
- Those that always occur on a specific weekday with a specific week within a specific month. (E.g. Mothers’ day is always the second Sunday in May.)
<Holiday name="Mothers' Day">
<Month>5</Month>
<DayOfWeek>0</DayOfWeek>
<WeekOfMonth>2</WeekOfMonth>
</Holiday>
- Those that always occur on the first weekday on or after a specified date. (E.g. Tax day is always the first weekday on or after April 15th.)
<Holiday name="Tax Day">
<WeekdayOnOrAfter>
<Month>4</Month>
<Day>15</Day>
</WeekdayOnOrAfter>
</Holiday>
- Those that always occur a specified number of days before or after another holiday. (E.g. Good Friday is always two days before Easter Sunday.) Note that the
Holiday
attribute in DaysAfterHoliday
must reference the name of another holiday that is defined in the XML file. If you’re defining a holiday as coming before another holiday, just use a negative number in the Days
property:
<Holiday name="Good Friday">
<DaysAfterHoliday Holiday="Easter">
<Days>-2</Days>
</DaysAfterHoliday>
</Holiday>
- Those that occur on a specified date but only on certain years. (E.g. in the United States, Inauguration day occurs on January 20 every four years.) Note that to use the
EveryXYears
node you must also include a StartYear
:
<Holiday name="Inauguration Day">
<Month>1</Month>
<Day>20</Day>
<EveryXYears>4</EveryXYears>
<StartYear>1940</StartYear>
</Holiday>
- Those that occur on a specified weekday in the last full week of a specified month. (E.g. Administrative Professionals day occurs on the Wednesday of the last full week of April.)
<Holiday name="Administrative Assistants' Day">
<LastFullWeekOfMonth>
<Month>4</Month>
<DayOfWeek>3</DayOfWeek>
</LastFullWeekOfMonth>
</Holiday>
- Easter - The algorithm for calculating the date of Easter Sunday is too different from any other holiday, so it is treated as its own holiday type. (Note: This returns the "Western" date for Easter. An implementation for the Eastern Orthodox Easter would be a good addition.)
<Holiday name="Easter">
<Easter />
</Holiday>
It is certainly true that not every day observed by all the people of the world can fit into these rules. Our objective was to meet 99% of the need. Of course, you could modify or extend this class to add additional capabilities.
Using the class
The constructor for the HolidayCalculator
class takes two parameters. The first is the DateTime that you want to begin your search for holidays on. The second is the path to your XML configuration file. The class has no public
methods. Rather, it has a property called OrderedHolidays
that returns an ArrayList
of Holiday
objects in date order. Each Holiday
object has two properties: Date
and Name
.
Using the sample application
Included in the code for download is a console application that simply asks the user to provide a date, then lists all of the holidays occurring in the following 12-month period (see figure 1).
The code
Below is the complete HolidayCalculator
class:
[Editor Note: Line breaks used to avoid scrolling.]
using System.Collections;
using System.Xml;
namespace JayMuntzCom
{
public class HolidayCalculator
{
#region Constructor
public HolidayCalculator(System.DateTime startDate, string xmlPath)
{
this.startingDate = startDate;
orderedHolidays = new ArrayList();
xHolidays = new XmlDocument();
xHolidays.Load(xmlPath);
this.processXML();
}
#endregion
#region Private Properties
private ArrayList orderedHolidays;
private XmlDocument xHolidays;
private DateTime startingDate;
#endregion
#region Public Properties
public ArrayList OrderedHolidays
{
get { return this.orderedHolidays; }
}
#endregion
#region Private Methods
private void processXML()
{
foreach (XmlNode n in xHolidays.SelectNodes("/Holidays/Holiday"))
{
Holiday h = this.processNode(n);
if (h.Date.Year > 1)
this.orderedHolidays.Add(h);
}
orderedHolidays.Sort();
}
private Holiday processNode(XmlNode n)
{
Holiday h = new Holiday();
h.Name = n.Attributes["name"].Value.ToString();
ArrayList childNodes = new ArrayList();
foreach (XmlNode o in n.ChildNodes)
{
childNodes.Add(o.Name.ToString());
}
if (childNodes.Contains("WeekOfMonth"))
{
int m = Int32.Parse(
n.SelectSingleNode("./Month").InnerXml.ToString());
int w = Int32.Parse(
n.SelectSingleNode("./WeekOfMonth").InnerXml.ToString());
int wd = Int32.Parse(
n.SelectSingleNode("./DayOfWeek").InnerXml.ToString());
h.Date = this.getDateByMonthWeekWeekday(m,w,wd,this.startingDate);
}
else if (childNodes.Contains("DayOfWeekOnOrAfter"))
{
int dow =
Int32.Parse(n.SelectSingleNode("./DayOfWeekOnOrAfter/DayOfWeek").
InnerXml.ToString());
if (dow > 6 || dow < 0)
throw new Exception("DOW is greater than 6");
int m =
Int32.Parse(n.SelectSingleNode("./DayOfWeekOnOrAfter/Month").
InnerXml.ToString());
int d =
Int32.Parse(n.SelectSingleNode("./DayOfWeekOnOrAfter/Day").
InnerXml.ToString());
h.Date = this.getDateByWeekdayOnOrAfter(dow,m,d, this.startingDate);
}
else if (childNodes.Contains("WeekdayOnOrAfter"))
{
int m =
Int32.Parse(n.SelectSingleNode("./WeekdayOnOrAfter/Month").
InnerXml.ToString());
int d =
Int32.Parse(n.SelectSingleNode("./WeekdayOnOrAfter/Day").
InnerXml.ToString());
DateTime dt = new DateTime(this.startingDate.Year, m, d);
if (dt < this.startingDate)
dt = dt.AddYears(1);
while(dt.DayOfWeek.Equals(DayOfWeek.Saturday) ||
dt.DayOfWeek.Equals(DayOfWeek.Sunday))
{
dt = dt.AddDays(1);
}
h.Date =dt;
}
else if (childNodes.Contains("LastFullWeekOfMonth"))
{
int m =
Int32.Parse(n.SelectSingleNode("./LastFullWeekOfMonth/Month").
InnerXml.ToString());
int weekday =
Int32.Parse(n.SelectSingleNode("./LastFullWeekOfMonth/DayOfWeek").
InnerXml.ToString());
DateTime dt = this.getDateByMonthWeekWeekday(m,5,weekday,
this.startingDate);
if (dt.AddDays(6-weekday).Month == m)
h.Date = dt;
else
h.Date = dt.AddDays(-7);
}
else if (childNodes.Contains("DaysAfterHoliday"))
{
XmlNode basis =
xHolidays.SelectSingleNode("/Holidays/Holiday[@name='" +
n.SelectSingleNode("./DaysAfterHoliday").Attributes["Holiday"].
Value.ToString() + "']");
Holiday bHoliday = this.processNode(basis);
int days =
Int32.Parse(
n.SelectSingleNode("./DaysAfterHoliday/Days").InnerXml.ToString());
h.Date = bHoliday.Date.AddDays(days);
}
else if (childNodes.Contains("Easter"))
{
h.Date = this.easter();
}
else
{
if (childNodes.Contains("Month") && childNodes.Contains("Day"))
{
int m =
Int32.Parse(n.SelectSingleNode("./Month").InnerXml.ToString());
int d =
Int32.Parse(n.SelectSingleNode("./Day").InnerXml.ToString());
DateTime dt = new DateTime(this.startingDate.Year, m, d);
if (dt < this.startingDate)
{
dt = dt.AddYears(1);
}
if (childNodes.Contains("EveryXYears"))
{
int yearMult =
Int32.Parse(
n.SelectSingleNode("./EveryXYears").InnerXml.ToString());
int startYear =
Int32.Parse(
n.SelectSingleNode("./StartYear").InnerXml.ToString());
if (((dt.Year - startYear) % yearMult) == 0)
{
h.Date = dt;
}
}
else
{
h.Date = dt;
}
}
}
return h;
}
private DateTime easter()
{
DateTime workDate = this.getFirstDayOfMonth(this.startingDate);
int y = workDate.Year;
if (workDate.Month > 4)
y = y+1;
return this.easter(y);
}
private DateTime easter(int y)
{
int a=y%19;
int b=y/100;
int c=y%100;
int d=b/4;
int e=b%4;
int f=(b+8)/25;
int g=(b-f+1)/3;
int h=(19*a+b-d-g+15)%30;
int i=c/4;
int k=c%4;
int l=(32+2*e+2*i-h-k)%7;
int m=(a+11*h+22*l)/451;
int easterMonth =(h+l-7*m+114)/31;
int p=(h+l-7*m+114)%31;
int easterDay=p+1;
DateTime est = new DateTime(y,easterMonth,easterDay);
if (est < this.startingDate)
return this.easter(y+1);
else
return new DateTime(y,easterMonth,easterDay);
}
private DateTime getDateByWeekdayOnOrAfter(int weekday,
int m, int d, DateTime startDate)
{
DateTime workDate = this.getFirstDayOfMonth(startDate);
while (workDate.Month != m)
{
workDate = workDate.AddMonths(1);
}
workDate = workDate.AddDays(d-1);
while (weekday != (int)(workDate.DayOfWeek))
{
workDate = workDate.AddDays(1);
}
if (workDate < this.startingDate)
return this.getDateByWeekdayOnOrAfter(weekday,m,d,
startDate.AddYears(1));
else
return workDate;
}
private DateTime getDateByMonthWeekWeekday(int month, int week,
int weekday, DateTime startDate)
{
DateTime workDate = this.getFirstDayOfMonth(startDate);
while (workDate.Month != month)
{
workDate = workDate.AddMonths(1);
}
while ((int)workDate.DayOfWeek != weekday)
{
workDate = workDate.AddDays(1);
}
DateTime result;
if (week == 1)
{
result = workDate;
}
else
{
int addDays = (week*7)-7;
int day = workDate.Day + addDays;
if (day > DateTime.DaysInMonth(workDate.Year,
workDate.Month))
{
day = day-7;
}
result = new DateTime(workDate.Year,workDate.Month,day);
}
if (result >= this.startingDate)
return result;
else
return this.getDateByMonthWeekWeekday(month,week,
weekday,startDate.AddYears(1));
}
private DateTime getFirstDayOfMonth(DateTime dt)
{
return new DateTime(dt.Year, dt.Month, 1);
}
#endregion
#region Holiday Object
public class Holiday : IComparable
{
public System.DateTime Date;
public string Name;
#region IComparable Members
public int CompareTo(object obj)
{
if (obj is Holiday)
{
Holiday h = (Holiday)obj;
return this.Date.CompareTo(h.Date);
}
throw new ArgumentException("Object is not a Holiday");
}
#endregion
}
#endregion
}
}
Conclusion
I did a fair amount of searching online for an example of determining the dates of holidays without any luck. This surprised me since my gut tells me that this problem must come up frequently. Perhaps the perfect solution (much better than mine) is out there and I missed it. However, I think I may have also discovered why nobody has posted the solution to this problem before.
I'm certain that I have overlooked certain holidays and the methods for calculating them. I wrote this class specifically to meet the needs of the specific application I'm working on. For that reason, there's a good chance that it will not perfectly meet the needs of others. I suppose I have learnt that this is a tougher problem than I originally thought. I'm very interested in feedback related to what I've presented here. I'm particularly interested to know if this class (or just the ideas contained within) meets any needs that are out there. Also, if this problem has been written about before, I'd be interested to know about it.
Acknowledgements
Marcos J. Montes’s The American Secular Holiday Calendar was an invaluable resource for both getting a list of typical American holidays and for the algorithm for calculating the date of Easter.
History
- 1st January, 2006
- Changed "
DateTime.Parse()
" statements to "new DateTime()
" to enable the code to work correctly under any System.Globalization.CultureInfo
setting.
- 17th September, 2005