Click here to Skip to main content
15,881,516 members
Articles / Web Development / ASP.NET
Article

Dynamic Holiday Date Calculator

Rate me:
Please Sign up or sign in to vote.
4.91/5 (51 votes)
4 Jan 2006MIT5 min read 258.5K   2.9K   102   60
A class to calculate what date the configured holidays fall on in different years.

Image 1

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.)
    XML
    <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.)
    XML
    <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.)
    XML
    <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:
    XML
    <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:
    XML
    <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.)
    XML
    <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.)
    XML
    <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.]

C#
using System.Collections;
using System.Xml;

namespace JayMuntzCom
{
  public class HolidayCalculator
  {
    #region Constructor
    /// <summary>
    /// Returns all of the holidays occuring in the year following the
    /// date that is passed in the constructor. Holidays are defined in
    /// an XML file.
    /// </summary>

    /// <param name="startDate">The starting date for
    /// returning holidays. All holidays for one year after this date
    /// are returned.</param>
    /// <param name="xmlPath">The path to the XML file
    /// that contains the holiday definitions.</param>
    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

    /// <summary>
    /// The holidays occuring after StartDate listed in
    /// chronological order;
    /// </summary>
    public ArrayList OrderedHolidays
    {
      get { return this.orderedHolidays; }
    }
    #endregion

    #region Private Methods


    /// <summary>

    /// Loops through the holidays defined in the XML configuration file,
    /// and adds the next occurance into the OrderHolidays collection if
    /// it occurs within one year.
    /// </summary>
    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();
    }


    /// <summary>
    /// Processes a Holiday node from the XML configuration file.
    /// </summary>
    /// <param name="n">The Holdiay node to process.</param>

    /// <returns></returns>
    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;
    }


    /// <summary>

    /// Determines the next occurance of Easter (western Christian).
    /// </summary>
    /// <returns></returns>
    private DateTime easter()
    {
      DateTime workDate = this.getFirstDayOfMonth(this.startingDate);
      int y = workDate.Year;
      if (workDate.Month > 4)
        y = y+1;
      return this.easter(y);
    }


    /// <summary>
    /// Determines the occurance of Easter in the given year. If the
    /// result comes before StartDate, recalculates for the following
    /// year.
    /// </summary>

    /// <param name="y"></param>
    /// <returns></returns>
    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);
    }

    /// <summary>
    /// Gets the next occurance of a weekday after
    /// a given month and day in the
    /// year after StartDate.
    /// </summary>

    /// <param name="weekday">The day of the
    /// week (0=Sunday).</param>
    /// <param name="m">The Month</param>
    /// <param name="d">Day</param>

    /// <returns></returns>
    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);
      }

      //It's possible the resulting date is before
      //the specified starting date.
      //If so we'll calculate again for the next year.
      if (workDate < this.startingDate)
        return this.getDateByWeekdayOnOrAfter(weekday,m,d,
                                         startDate.AddYears(1));
      else
        return workDate;
    }

    /// <summary>
    /// Gets the n'th instance of a day-of-week
    /// in the given month after StartDate
    /// </summary>
    /// <param name="month">The month the
    /// Holiday falls on.</param>

    /// <param name="week">The instance of
    /// weekday that the Holiday
    /// falls on (5=last instance in the month).</param>
    /// <param name="weekday">The day of
    /// the week that the Holiday falls
    /// on.</param>
    /// <returns></returns>

    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);
      }

      //It's possible the resulting date is
      //before the specified starting date.
      //If so we'll calculate again for the next year.
      if (result >= this.startingDate)
        return result;
      else
        return this.getDateByMonthWeekWeekday(month,week,
                                  weekday,startDate.AddYears(1));


    }

    /// <summary>
    /// Returns the first day of the month for the specified date.
    /// </summary>
    /// <param name="dt"></param>
    /// <returns></returns>

    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
    • Article submitted.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionPerfect! Pin
Amos VanHorn13-Sep-21 10:04
Amos VanHorn13-Sep-21 10:04 
QuestionFYI Setting the Start Date Pin
Mike Lucas6-Feb-18 5:48
Mike Lucas6-Feb-18 5:48 
Generalexcellant Pin
chait30116-Aug-14 3:31
chait30116-Aug-14 3:31 
GeneralMy vote of 5 Pin
fredatcodeproject12-Nov-13 22:02
professionalfredatcodeproject12-Nov-13 22:02 
SuggestionWhat about adjust for weekend; Sat to Fri and Sun to Mon Pin
Schporter19-Aug-13 16:59
Schporter19-Aug-13 16:59 
GeneralRe: What about adjust for weekend; Sat to Fri and Sun to Mon Pin
Gary Turner21-Jan-14 18:45
Gary Turner21-Jan-14 18:45 
GeneralMy vote of 5 Pin
karenpayne17-Jul-12 10:33
karenpayne17-Jul-12 10:33 
GeneralMy vote of 5 Pin
MackZero17-Dec-10 9:24
MackZero17-Dec-10 9:24 
QuestionPassover Pin
CRhoa1253511-Nov-10 4:44
CRhoa1253511-Nov-10 4:44 
GeneralEaster day calcualtion Pin
AlexEvans26-Aug-08 16:25
AlexEvans26-Aug-08 16:25 
GeneralRe: Easter day calcualtion Pin
Jay Muntz27-Aug-08 4:15
Jay Muntz27-Aug-08 4:15 
GeneralRe: Easter day calcualtion Pin
DevilSun17-Mar-10 12:15
DevilSun17-Mar-10 12:15 
Generaluse Date format in Holiday Pin
MrJohn1231-Aug-08 17:48
MrJohn1231-Aug-08 17:48 
GeneralRe: use Date format in Holiday Pin
MrJohn1231-Aug-08 17:54
MrJohn1231-Aug-08 17:54 
GeneralVictoria Day (Canada) Pin
Ghislain Hivon19-Jun-08 8:24
Ghislain Hivon19-Jun-08 8:24 
GeneralRe: Victoria Day (Canada) Pin
boyhsu21-May-13 14:39
boyhsu21-May-13 14:39 
GeneralRe: Victoria Day (Canada) Pin
Ghislain Hivon22-May-13 1:35
Ghislain Hivon22-May-13 1:35 
GeneralRe: Victoria Day (Canada) Pin
boyhsu28-May-13 12:31
boyhsu28-May-13 12:31 
GeneralRe: Victoria Day (Canada) Pin
Member 1008081128-May-13 10:32
Member 1008081128-May-13 10:32 
GeneralRe: Victoria Day (Canada) Pin
Member 1247374520-Apr-16 7:02
Member 1247374520-Apr-16 7:02 
GeneralEastern Orthodox Easter Pin
Mikhail Diatchenko7-Jun-08 13:07
Mikhail Diatchenko7-Jun-08 13:07 
GeneralThanksgiving Pin
Juliensobrier2-Jun-08 5:16
Juliensobrier2-Jun-08 5:16 
GeneralRe: Thanksgiving Pin
cmpalmer225-Feb-09 11:14
cmpalmer225-Feb-09 11:14 
GeneralVery Good, good Pin
crisyuri8-Nov-06 6:22
crisyuri8-Nov-06 6:22 
GeneralAnother thank you! Pin
richid25-May-06 5:07
richid25-May-06 5:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.