工作日计算器_.NET工作日计算器

工作日计算器

Often times in development shops which focus on line-of-business software, the need for determining next and previous working days--that is, days which are neither weekends nor observed holidays--exists. Developers may need to know the next day that a banking institution will process a payment, or the next business day for their company, or the most recent working day that has already passed. None of these are available in libraries of the Framework. However, with a few lines of code we can create our own such library for determining these days.

通常,在专注于业务线软件的开发商店中,存在确定下一个和上一个工作日(即既不是周末也不是法定假日的日子)的需求。 开发人员可能需要知道银行机构将在第二天处理付款,其公司的下一个工作日或已经过去的最近工作日。 这些都没有在框架的库中提供。 但是,通过几行代码,我们可以创建自己的此类库来确定这些日子。

The code samples and the Visual Studio project referenced throughout this article will be written in C#. It is left to the reader as an exercise to convert the code to Visual Basic .NET should they desire to do so. The code is not exotic, so it should be easily converted using one of the online conversion tools such as Telerik's Code Converter. The complete project will be linked at the end of this article.

本文中引用的代码示例和Visual Studio项目将用C#编写。 读者可以根据需要练习将代码转换为Visual Basic .NET。 该代码不是稀奇古怪的,因此应该使用一种在线转换工具(例如Telerik的Code Converter)轻松地对其进行转换 。 完整的项目将在本文结尾处链接。

The two primary elements of determining whether or not an arbitrary day is a working day are determining whether or not we are dealing with a weekend or a holiday. Weekends are easy:  That bit actually is built into the Framework via the DateTime structure's DayOfWeek property. How do we deal with holidays, then? This part we must write ourselves.

确定任意一天是否为工作日的两个主要要素是确定我们是否要处理周末或假日。 周末很容易:实际上是通过DateTime结构DayOfWeek属性将其内置到Framework中的。 那我们该如何处理假期? 这部分我们必须写自己。

The first step in determining how to calculate holidays would be to determine what holidays do we actually care about? For simplicity, I am going to stick with those dates which are commonly observed within the United States (U.S.):

确定假期的计算的第一步将是确定我们实际上关心的假期是什么? 为简单起见,我将坚持在美国(US)中通常观察到的那些日期:

  • New Year's Day

    元旦
  • Martin Luther King, Jr. Day

    小马丁·路德·金
  • President's Day

    总统日
  • Memorial Day

    纪念日
  • Independence Day

    独立日
  • Labor Day

    劳动节
  • Columbus Day

    哥伦布日
  • Veteran's Day

    退伍军人节
  • Thanksgiving Day

    感恩节
  • Christmas Day

    圣诞节

A couple of these days, depending on the company's holiday policy, may actually enjoy additional days over the actual observed day. This will be discussed later in the article. A quick search of the Internet will tell us on which day each of the above holidays fall. What we notice is that some holidays always fall on the same numeric day of the same month (e.g. Christmas is always the 25th of December), and some holidays fall on the an ordinal (e.g. Labor Day is the first Monday of September). The former will be easy to craft:  Just create a new instance of a DateTime structure, and pass it the numeric day. For the latter, though, we will need a way of calculating ordinals. First, let us define an enum structure to define what ordinals we care about:

根据公司的假期政策,这几天可能实际上会比实际观察到的日期多出几天。 这将在本文后面讨论。 快速搜索互联网可以告诉我们以上每个假期的具体日期。 我们注意到的是,某些假期总是落在同一个月的同一数字天( DateTime结构的新实例,然后将其传递给数字日期即可。 但是,对于后者,我们将需要一种计算序数的方法。 首先,让我们定义一个枚举结构来定义我们关心的序数:

namespace WorkingDayCalculator.Core.Enums
{
    public enum Ordinal
    {
        // The library uses these values in mathematical calculations. The values
        // that are set are important and should not be changed unless the developer
        // is going to modify the math calculations which rely on these values.

        Last = -1,
        First = 0,
        Second = 1,
        Third = 2,
        Fourth = 3,
        Fifth = 4,
    }
}

We will use this enum in our calculations for holidays like Labor Day. We have the items Last and Fifth defined, but we won't be using them for our calculations. They are included for those individuals who might like to know what the last Friday of January is or what the fifth Monday of September is.

我们将在劳动节等假期的计算中使用此枚举。 我们已经定义了

Next, we will define a couple of utility functions for calculating ordinal dates, making use of the enum above:

接下来,我们将使用上面的枚举定义几个实用程序函数来计算序数日期:

using System;
using WorkingDayCalculator.Core.Enums;

namespace WorkingDayCalculator.Core
{
    public static class DateHelper
    {
        public static DateTime GetOrdinalDate(int year, Month month, Ordinal ordinal, DayOfWeek dayOfWeek)
        {
            DateTime ordinalDate;

            if (ordinal == Ordinal.Last)
            {
                DateTime startDate = (new DateTime(year, (int)month + 1, 1)).AddDays(-1);
                int offsetFromDayOfWeek = GetPastOffsetFromTarget(startDate.DayOfWeek, dayOfWeek);

                ordinalDate = startDate.AddDays(-1 * offsetFromDayOfWeek);
            }
            else
            {
                DateTime startDate = new DateTime(year, (int)month, 1);
                int offsetFromDayOfWeek = GetFutureOffsetFromTarget(startDate.DayOfWeek, dayOfWeek);
                int weeksToAdd = (int)ordinal * 7;

                ordinalDate = startDate.AddDays(offsetFromDayOfWeek + weeksToAdd);

                if (ordinalDate.Month != (int)month)
                {
                    throw new ArgumentException("Not enough days in month to satisfy ordinal requirement.");
                }
            }

            return ordinalDate;
        }

        public static int GetFutureOffsetFromTarget(DayOfWeek day, DayOfWeek target)
        {
            return ((int)(DayOfWeek.Saturday - day) + (int)target + 1) % 7;
        }

        public static int GetPastOffsetFromTarget(DayOfWeek day, DayOfWeek target)
        {
            return ((int)day + (int)DayOfWeek.Saturday - (int)target + 1) % 7;
        }
    }
}

The GetOrdinalDate method will take in the different parts of a date as well as an Ordinal value, and it will calculate the date that matches that criteria. It does this by making use of the other two helper methods:  GetFutureOffsetFromTarget and GetPastOffsetFromTarget. What each of these methods do is perform a mathematical calculation of the difference in days between the current day of week (parameter) and the target date we are trying to reach. This difference is then added to the startDate to arrive at the result date. It is important to be aware of the fact that we are relying on the integer values of the DayOfWeek enum within our mathematical calculations. It is unlikely that Microsoft would change the values of the enum members, but we could certainly define our own enum to get around this concern. Also, if the Ordinal value that we passed in is Last, then our start date is figured by beginning with the first day of the next month, and then subtracting one day. Otherwise, we simply start with the first day of the target month.

数值,并将计算与该条件匹配的日期。 它通过使用其他两个辅助方法来做到这一点: DayOfWeek枚举的整数值。 Microsoft不太可能更改枚举成员的值,但是我们可以定义自己的枚举来解决此问题。 另外,如果我们传入的Ordinal值为

Now that we know how to calculate specific numeric holidays, and now that we have a handy utility function for calculating ordinal dates, we can focus on each individual holiday. Searching the Internet, we can find the rules for each holiday. Then it's simply a matter of writing each of those rules in code. Let us look at New Year's Day and Labor Day as examples:

现在我们知道了如何计算特定的数字假日,并且现在有了用于计算序数日期的便捷实用函数,我们可以专注于每个假日。 搜索互联网,我们可以找到每个假期的规则。 然后,只需在代码中编写每个规则即可。 让我们以元旦和劳动节为例:

public static DateTime GetNewYearsDay(int year, HolidayOptions options = null)
{
    DateTime holiday = new DateTime(year, (int)Holiday.NewYearsDayMonth, 1);
 
    holiday = AdjustIfObserved(holiday, options, Holidays.NewYearsDay);
 
    return holiday;
}

public static DateTime GetLaborDay(int year, HolidayOptions options = null)
{
    DateTime holiday = DateHelper.GetOrdinalDate(year, Holiday.LaborDayMonth, Ordinal.First, DayOfWeek.Monday);
 
    holiday = AdjustIfObserved(holiday, options, Holidays.LaborDay);
 
    return holiday;
}

In the above you can see that because New Year's Day is a specific numeric day, we simply pass that day value (i.e. 1) to the DateTime constructor. For Labor Day, however, we need to determine an ordinal value. We pass in the an Ordinal value of First since that is what the rule for Labor Day specifies. The remaining holidays are calculated in a similar fashion following either of these two approaches.

在上面您可以看到,由于元旦是一个特定的数字日,因此我们只需将该日期值(即1)传递给DateTime构造函数即可。 但是,对于劳动节,我们需要确定一个序数值。 我们输入“ 数值,因为这是劳动节的规则所指定的。 按照这两种方法中的任何一种,剩余假期的计算方式相似。

We next need to define methods that will allow us to test whether or not a date falls on a holiday. For the holidays themselves, we can define additional helper methods. Taking New Year's Day as an example:

接下来,我们需要定义允许我们测试日期是否为假期的方法。 对于假期本身,我们可以定义其他帮助程序方法。 以元旦为例:

public static bool IsNewYearsDay(DateTime date, HolidayOptions options = null)
{
    DateTime holiday = GetNewYearsDay(date.Year, options);

    return (holiday == date.Date);
}

...we can see that we call the GetNewYearsDay method that we defined above, and we take the date it returns and compare it to the incoming date. If the date is identical, then we are dealing with New Year's Day. We define similar methods for the remaining holidays. We can then define an additional helper method:

...我们可以看到我们调用了上面定义的

public static bool IsHoliday(DateTime date, HolidayOptions options = null)
{
    HolidayOptions safeOptions = options ?? new HolidayOptions();
    Holidays holidaysToCheck = safeOptions.HolidaysToCheck;

    // Month check saves us a call to the function if we know that we're not in
    //   the correct month anyway.

    // It's possible that we may have holidays that are checked for more frequently.
    //   We could rearrange these in order of precedence/frequency, but that's probably
    //   a micro-optimization if anything.

    return (date.Month == (int)Holiday.ChristmasDayMonth && holidaysToCheck.HasFlag(Holidays.ChristmasDay) && IsChristmasDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.ColumbusDayMonth && holidaysToCheck.HasFlag(Holidays.ColumbusDay) && IsColumbusDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.IndependenceDayMonth && holidaysToCheck.HasFlag(Holidays.IndependenceDay) && IsIndependenceDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.LaborDayMonth && holidaysToCheck.HasFlag(Holidays.LaborDay) && IsLaborDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.MartinLutherKingDayMonth && holidaysToCheck.HasFlag(Holidays.MartinLutherKingDay) && IsMartinLutherKingDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.MemorialDayMonth && holidaysToCheck.HasFlag(Holidays.MemorialDay) && IsMemorialDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.NewYearsDayMonth && holidaysToCheck.HasFlag(Holidays.NewYearsDay) && IsNewYearsDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.PresidentsDayMonth && holidaysToCheck.HasFlag(Holidays.PresidentsDay) && IsPresidentsDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.ThanksgivingDayMonth && holidaysToCheck.HasFlag(Holidays.ThanksgivingDay) && IsThanksgivingDay(date, safeOptions)) ||
            (date.Month == (int)Holiday.VeteransDayMonth && holidaysToCheck.HasFlag(Holidays.VeteransDay) && IsVeteransDay(date, safeOptions));
}

...that will check the incoming date against each defined holiday. We can order these methods however we like; short-circuiting in C# will ensure that once we find a match we will exit the conditional test. The month check at the start of each condition is a simple test to bypass going into the IsxxxxxDay methods needlessly. All of these helpers will be combined into our working day calculator.

...将对照每个定义的假日检查输入日期。 我们可以根据需要订购这些方法。 C#中的短路将确保一旦找到匹配项,我们将退出条件测试。 每个条件开始时的月份检查都是一项简单的测试,可以绕过不必要地进入

We defined methods to calculate ordinal dates within a month, and we defined methods to calculate individual holidays. Next we will use these methods to define our working day calculators. We know that working days will not be weekends, and we know that working days will not be holidays. Using what we have already defined, we could construct methods as follows:

我们定义了一种方法来计算一个月内的序数日期,并且定义了一种方法来计算各个假期。 接下来,我们将使用这些方法来定义工作日计算器。 我们知道工作日将不会是周末,并且我们知道工作日将不会是假期。 使用我们已经定义的方法,我们可以构建如下的方法:

using System;

namespace WorkingDayCalculator.Core
{
    public static class WorkingDay
    {
        public static DateTime GetNextWorkingDay(DateTime date, HolidayOptions options = null)
        {
            return GetNextWorkingDay(date, Holiday.IsHoliday, options);
        }

        public static DateTime GetNextWorkingDay(DateTime date, Func<DateTime, HolidayOptions, bool> holidayCalculator, HolidayOptions options = null)
        {
            DateTime workingDay = GetWorkingDayUsingOffset(date, 1, holidayCalculator, options);

            return workingDay;
        }

        public static DateTime GetPreviousWorkingDay(DateTime date, HolidayOptions options = null)
        {
            return GetPreviousWorkingDay(date, Holiday.IsHoliday, options);
        }

        public static DateTime GetPreviousWorkingDay(DateTime date, Func<DateTime, HolidayOptions, bool> holidayCalculator, HolidayOptions options = null)
        {
            DateTime workingDay = GetWorkingDayUsingOffset(date, -1, holidayCalculator, options);

            return workingDay;
        }

        private static DateTime GetWorkingDayUsingOffset(DateTime date, int offset, Func<DateTime, HolidayOptions, bool> holidayCalculator, HolidayOptions options = null)
        {
            DateTime workingDay = date.AddDays(offset);

            while (workingDay.DayOfWeek == DayOfWeek.Saturday || workingDay.DayOfWeek == DayOfWeek.Sunday || holidayCalculator(workingDay, options))
            {
                workingDay = workingDay.AddDays(offset);
            }

            return workingDay;
        }
    }
}

We essentially have two methods:  GetNextWorkingDay and GetPreviousWorkingDay. Each of these call the helper method GetWorkingDayUsingOffset which does the "heavy lifting" of calculating the target working day. Within this helper method, we check the value of workingDay to determine if it is a weekend or a holiday. The holiday check is performed by way of a delegate. This delegate allows us, should we desire, to alter the way holidays are determined. This is the reason for the overloads for GetNextWorkingDay and GetPreviousWorkingDay. Each of the overloads also takes a delegate. So while we have defined a class for all of the U.S. holidays, we could define a second class which dealt with the holidays of another country, or perhaps we have two classes where one class is federal holidays and one class is state holidays. The delegate gives us the flexibility to alter how the GetWorkingDayUsingOffset completes its job.

我们实质上有两种方法:

You probably noticed the HolidayOptions parameter on the GetNextWorkingDay and GetPreviousWorkingDay methods. The intent of this class is to store options that can further affect how we calculate holidays.

您可能已经注意到HolidayOptions参数。 此类的目的是存储可能进一步影响我们计算假期的方式的选项。

Some companies will observe holidays which fall on a weekend on either the previous Friday or the subsequent Monday. Depending on how we set the Observance property, holidays will be adjusted to match this property's value. For example, if we set the value to ObservanceType.PostWeekend and we are trying to determine the next working day after December 23, 2016, because Christmas falls on a Sunday, and because we have a value of PostWeekend, then Monday the 26th will be seen as a holiday, resulting in Tuesday the 27th being returned as the next working day.

一些公司会观察到假日,假日是在上一个星期五或下一个星期一的周末。 根据我们如何设置“

Also, the HolidaysToCheck property presents a bit flag whereby we can skip over certain holidays when performing our holiday check. One could define all the known holidays in one class, and then he could simply create a bit mask to eliminate those holidays for which he did not want to check. It probably makes more sense to classify one's holidays by commonality, but the flexibility of having the bit flag is there for those who would prefer that approach.

另外,

I mentioned earlier that some holidays may actually be observed across multiple days. My organization, for example, treats Thanksgiving Day as a holiday as well as the Friday after. To account for this in the code above, one could define an additional holiday named DayAfterThanksgiving, for example. It would work like any other holiday we have defined. This would be the simplest way to handle such days. A more complicated way would be to modify the HolidayOptions class to include a setting that could be evaluated during the holiday calculations. Such an approach, however, would require additional coding to one or more of our classes.

我之前提到过,实际上可能会在几天内观察到一些假期。 例如,我的组织将感恩节和节假日当作假日。 为了在上面的代码中解决这个HolidayOptions类以包含可以在假日计算期间评估的设置。 但是,这种方法需要对我们的一个或多个类进行附加编码。

As you can see, we can create a working day calculator with just a few classes. In the approach outlined by this article, we have added some additional flexibility that will allow us to extend the calculator to cover other countries' holidays, or to allow us to vary our holiday calculations based on differences between states or jurisdictions. With these classes, we can now determine dates such as when the next bank processing day will be or what day an employee will be off for a holiday.

如您所见,我们可以创建一个只有几节课的工作日计算器。 在本文概述的方法中,我们增加了一些额外的灵活性,这使我们可以将计算器扩展到涵盖其他国家的假期,或者允许我们根据州或辖区之间的差异来更改假期计算。 通过这些课程,我们现在可以确定日期,例如下一个银行处理日是何时或员工休假日的日期。

The complete solution, including unit tests, can be downloaded from the following link:

完整的解决方案,包括单元测试,可以从以下链接下载:

https://filedb.experts-exchange.com/incoming/ee-stuff/8443-WorkingDayCalculator.zip https://filedb.experts-exchange.com/incoming/ee-stuff/8443-WorkingDayCalculator.zip

翻译自: https://www.experts-exchange.com/articles/26839/NET-Working-Day-Calculator.html

工作日计算器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值