动态节假日日期计算器
一个类,用于计算配置的假日将在不同年份落在什么日期。
引言
此类会返回在一整年中发生的所有假日。每个假日的定义都指定在一个XML配置文件中。此类可用于任何需要呈现特定日期即将到来的假日列表的情况。由于所有假日都定义在XML文件中,因此可以根据不同的文化和具体用途自定义应用程序。
问题所在
在我们的组织中,我们正在构建一个应用程序来帮助小型企业主管理自己的营销计划。该应用程序的一个功能是提醒用户即将到来的假日,以便他们可以提前规划基于这些日期的销售和促销活动。当我们提出这个解决方案时,我们的目标是创建一种方法来设置应用程序要使用的假日,而无需再担心它。其中的难点在于,许多假日每年的日期都不同,而且决定它们日期的规则也多种多样。
解决方案
我们决定最好将所有不同假日的规则存储在一个XML文件中,然后编写一个可以解析这些规则并计算每个假日的正确日期的类。为了做到这一点,我们不得不将假日分成不同的类型,这些类型以相似的方式计算。以下是我们处理的不同类型的假日以及每种类型的示例XML配置
- 每年都在同一个月和同一天发生的假日。(例如:土拨鼠日总是2月2日。)
<Holiday name="Groundhog Day"> <Month>2</Month> <Day>2</Day> </Holiday>
- 总是在特定月份的特定星期几的特定星期几发生的假日。(例如:母亲节总是五月的第二个星期日。)
<Holiday name="Mothers' Day"> <Month>5</Month> <DayOfWeek>0</DayOfWeek> <WeekOfMonth>2</WeekOfMonth> </Holiday>
- 总是在指定日期或之后第一个工作日发生的假日。(例如:报税日总是4月15日或之后第一个工作日。)
<Holiday name="Tax Day"> <WeekdayOnOrAfter> <Month>4</Month> <Day>15</Day> </WeekdayOnOrAfter> </Holiday>
- 总是在另一个假日之前或之后指定天数发生的假日。(例如:耶稣受难日总是在复活节星期日的前两天。)请注意,
DaysAfterHoliday
中的Holiday
属性必须引用XML文件中定义的另一个假日的名称。如果您将假日定义为在另一个假日前发生,只需在Days
属性中使用负数。<Holiday name="Good Friday"> <DaysAfterHoliday Holiday="Easter"> <Days>-2</Days> </DaysAfterHoliday> </Holiday>
- 在指定日期发生,但仅在某些年份发生的假日。(例如:在美国,就职日每四年在1月20日发生。)请注意,要使用
EveryXYears
节点,您还必须包含一个StartYear
。<Holiday name="Inauguration Day"> <Month>1</Month> <Day>20</Day> <EveryXYears>4</EveryXYears> <StartYear>1940</StartYear> </Holiday>
- 在指定月份的最后一个完整星期中发生的特定星期几的假日。(例如:行政专业人士日发生在四月的最后一个完整星期中的星期三。)
<Holiday name="Administrative Assistants' Day"> <LastFullWeekOfMonth> <Month>4</Month> <DayOfWeek>3</DayOfWeek> </LastFullWeekOfMonth> </Holiday>
- 复活节 - 计算复活节星期日日期的算法与其他假日大不相同,因此它被视为一种特殊的假日类型。(注意:这返回的是复活节的“西方”日期。实现东正教复活节将是一个很好的补充。)
<Holiday name="Easter"> <Easter /> </Holiday>
诚然,并非世界上所有人都庆祝的每一个日子都能套用这些规则。我们的目标是满足99%的需求。当然,您可以修改或扩展此类以增加其他功能。
使用该类
HolidayCalculator
类的构造函数接受两个参数。第一个是要开始搜索假日的DateTime。第二个是XML配置文件路径。该类没有public
方法。相反,它有一个名为OrderedHolidays
的属性,该属性返回一个按日期排序的Holiday
对象ArrayList
。每个Holiday
对象有两个属性:Date
和Name
。
使用示例应用程序
下载的代码中包含一个控制台应用程序,该应用程序只要求用户提供一个日期,然后列出接下来的12个月内发生的所有假日(参见图1)。
代码
以下是完整的HolidayCalculator
类
[编辑注释:换行以避免滚动。]
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
}
}
结论
我在网上搜索了很多关于如何在不查找的情况下确定假日日期的例子。这让我很惊讶,因为我的直觉告诉我这个问题肯定经常出现。也许完美的解决方案(比我的好得多)就在那里,我错过了。但是,我也可能发现了为什么没有人之前发布过这个问题的解决方案。
我敢肯定我忽略了某些假日以及计算它们的方法。我编写此类是为了满足我正在处理的特定应用程序的需求。因此,它很可能无法完全满足他人的需求。我想我已经了解到,这是一个比我最初想象的更棘手的问题。我非常乐意收到与我在此展示的内容相关的反馈。我特别想知道这个类(或其中的想法)是否能满足现有的任何需求。另外,如果这个问题以前被写过,我也很有兴趣知道。
致谢
Marcos J. Montes的美国世俗假日日历是获取典型美国假日列表以及计算复活节日期算法的宝贵资源。
历史
- 2006年1月1日
- 将“
DateTime.Parse()
”语句更改为“new DateTime()
”,以便代码在任何System.Globalization.CultureInfo
设置下都能正确工作。
- 将“
- 2005年9月17日
- 文章已提交。