Calculate the number of business days between two dates?

后端 未结 30 1196
悲&欢浪女
悲&欢浪女 2020-11-22 14:54

In C#, how can I calculate the number of business (or weekdays) days between two dates?

相关标签:
30条回答
  • 2020-11-22 15:36

    I just improved @Alexander and @Slauma answer to support a business week as a parameter, for cases where saturday is a business day, or even cases where there is just a couple of days of the week that are considered business days:

    /// <summary>
    /// Calculate the number of business days between two dates, considering:
    ///  - Days of the week that are not considered business days.
    ///  - Holidays between these two dates.
    /// </summary>
    /// <param name="fDay">First day of the desired 'span'.</param>
    /// <param name="lDay">Last day of the desired 'span'.</param>
    /// <param name="BusinessDaysOfWeek">Days of the week that are considered to be business days, if NULL considers monday, tuesday, wednesday, thursday and friday as business days of the week.</param>
    /// <param name="Holidays">Holidays, if NULL, considers no holiday.</param>
    /// <returns>Number of business days during the 'span'</returns>
    public static int BusinessDaysUntil(this DateTime fDay, DateTime lDay, DayOfWeek[] BusinessDaysOfWeek = null, DateTime[] Holidays = null)
    {
        if (BusinessDaysOfWeek == null)
            BusinessDaysOfWeek = new DayOfWeek[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday };
        if (Holidays == null)
            Holidays = new DateTime[] { };
    
        fDay = fDay.Date;
        lDay = lDay.Date;
    
        if (fDay > lDay)
            throw new ArgumentException("Incorrect last day " + lDay);
    
        int bDays = (lDay - fDay).Days + 1;
        int fullWeekCount = bDays / 7;
        int fullWeekCountMult = 7 - WeekDays.Length;
        //  Find out if there are weekends during the time exceedng the full weeks
        if (bDays > (fullWeekCount * 7))
        {
            int fDayOfWeek = (int)fDay.DayOfWeek;
            int lDayOfWeek = (int)lDay.DayOfWeek;
    
            if (fDayOfWeek > lDayOfWeek)
                lDayOfWeek += 7;
    
            // If they are the same, we already covered it right before the Holiday subtraction
            if (lDayOfWeek != fDayOfWeek)
            {
                //  Here we need to see if any of the days between are considered business days
                for (int i = fDayOfWeek; i <= lDayOfWeek; i++)
                    if (!WeekDays.Contains((DayOfWeek)(i > 6 ? i - 7 : i)))
                        bDays -= 1;
            }
        }
    
        //  Subtract the days that are not in WeekDays[] during the full weeks in the interval
        bDays -= (fullWeekCount * fullWeekCountMult);
        //  Subtract the number of bank holidays during the time interval
        bDays = bDays - Holidays.Select(x => x.Date).Count(x => fDay <= x && x <= lDay);
    
        return bDays;
    }
    
    0 讨论(0)
  • 2020-11-22 15:39

    I've had such a task before and I've got the solution. I would avoid enumerating all days in between when it's avoidable, which is the case here. I don't even mention creating a bunch of DateTime instances, as I saw in one of the answers above. This is really waste of processing power. Especially in the real world situation, when you have to examine time intervals of several months. See my code, with comments, below.

        /// <summary>
        /// Calculates number of business days, taking into account:
        ///  - weekends (Saturdays and Sundays)
        ///  - bank holidays in the middle of the week
        /// </summary>
        /// <param name="firstDay">First day in the time interval</param>
        /// <param name="lastDay">Last day in the time interval</param>
        /// <param name="bankHolidays">List of bank holidays excluding weekends</param>
        /// <returns>Number of business days during the 'span'</returns>
        public static int BusinessDaysUntil(this DateTime firstDay, DateTime lastDay, params DateTime[] bankHolidays)
        {
            firstDay = firstDay.Date;
            lastDay = lastDay.Date;
            if (firstDay > lastDay)
                throw new ArgumentException("Incorrect last day " + lastDay);
    
            TimeSpan span = lastDay - firstDay;
            int businessDays = span.Days + 1;
            int fullWeekCount = businessDays / 7;
            // find out if there are weekends during the time exceedng the full weeks
            if (businessDays > fullWeekCount*7)
            {
                // we are here to find out if there is a 1-day or 2-days weekend
                // in the time interval remaining after subtracting the complete weeks
                int firstDayOfWeek = (int) firstDay.DayOfWeek;
                int lastDayOfWeek = (int) lastDay.DayOfWeek;
                if (lastDayOfWeek < firstDayOfWeek)
                    lastDayOfWeek += 7;
                if (firstDayOfWeek <= 6)
                {
                    if (lastDayOfWeek >= 7)// Both Saturday and Sunday are in the remaining time interval
                        businessDays -= 2;
                    else if (lastDayOfWeek >= 6)// Only Saturday is in the remaining time interval
                        businessDays -= 1;
                }
                else if (firstDayOfWeek <= 7 && lastDayOfWeek >= 7)// Only Sunday is in the remaining time interval
                    businessDays -= 1;
            }
    
            // subtract the weekends during the full weeks in the interval
            businessDays -= fullWeekCount + fullWeekCount;
    
            // subtract the number of bank holidays during the time interval
            foreach (DateTime bankHoliday in bankHolidays)
            {
                DateTime bh = bankHoliday.Date;
                if (firstDay <= bh && bh <= lastDay)
                    --businessDays;
            }
    
            return businessDays;
        }
    

    Edit by Slauma, August 2011

    Great answer! There is little bug though. I take the freedom to edit this answer since the answerer is absent since 2009.

    The code above assumes that DayOfWeek.Sunday has the value 7 which is not the case. The value is actually 0. It leads to a wrong calculation if for example firstDay and lastDay are both the same Sunday. The method returns 1 in this case but it should be 0.

    Easiest fix for this bug: Replace in the code above the lines where firstDayOfWeek and lastDayOfWeek are declared by the following:

    int firstDayOfWeek = firstDay.DayOfWeek == DayOfWeek.Sunday 
        ? 7 : (int)firstDay.DayOfWeek;
    int lastDayOfWeek = lastDay.DayOfWeek == DayOfWeek.Sunday
        ? 7 : (int)lastDay.DayOfWeek;
    

    Now the result is:

    • Friday to Friday -> 1
    • Saturday to Saturday -> 0
    • Sunday to Sunday -> 0
    • Friday to Saturday -> 1
    • Friday to Sunday -> 1
    • Friday to Monday -> 2
    • Saturday to Monday -> 1
    • Sunday to Monday -> 1
    • Monday to Monday -> 1
    0 讨论(0)
  • 2020-11-22 15:39
    public static int CalculateBusinessDaysInRange(this DateTime startDate, DateTime endDate, params DateTime[] holidayDates)
    {
        endDate = endDate.Date;
        if(startDate > endDate)
            throw new ArgumentException("The end date can not be before the start date!", nameof(endDate));
        int accumulator = 0;
        DateTime itterator = startDate.Date;
        do 
        {
            if(itterator.DayOfWeek != DayOfWeek.Saturday && itterator.DayOfWeek != DayOfWeek.Sunday && !holidayDates.Any(hol => hol.Date == itterator))
            { accumulator++; }
        } 
        while((itterator = itterator.AddDays(1)).Date <= endDate);
        return accumulator
    }
    

    I'm only posting this because despite all of the excellent answers that have been given, none of the math made sense to me. This is definitely a KISS method that should work and be fairly maintainable. Granted if you are calculating ranges that are greater than 2-3 months this will not be the most effective way. We simply determine if it is a Saturday or Sunday or the date is a given holiday date. If it's not we add a business day. If it is then everything is fine.

    I'm sure this could be even more so simplified with LINQ, but this way is much easier to understand.

    0 讨论(0)
  • 2020-11-22 15:40

    Well this has been beaten to death. :) However I'm still going to provide another answer because I needed something a bit different. This solution is different in that it returns a Business TimeSpan between the start and end, and you can set the business hours of the day, and add holidays. So you can use it to calculate if it happens within a day, across days, over weekends, and even holidays. And you can get just the business days or not by just getting what you need from the returned TimeSpan object. And the way it uses lists of days, you can see how very easy it would be to add the list of non-work days if it's not the typical Sat and Sun. And I tested for a year, and it seems super fast.

    I just hope the pasting of the code is accurate. But I know it works.

    public static TimeSpan GetBusinessTimespanBetween(
        DateTime start, DateTime end,
        TimeSpan workdayStartTime, TimeSpan workdayEndTime,
        List<DateTime> holidays = null)
    {
        if (end < start)
            throw new ArgumentException("start datetime must be before end datetime.");
    
        // Just create an empty list for easier coding.
        if (holidays == null) holidays = new List<DateTime>();
    
        if (holidays.Where(x => x.TimeOfDay.Ticks > 0).Any())
            throw new ArgumentException("holidays can not have a TimeOfDay, only the Date.");
    
        var nonWorkDays = new List<DayOfWeek>() { DayOfWeek.Saturday, DayOfWeek.Sunday };
    
        var startTime = start.TimeOfDay;
    
        // If the start time is before the starting hours, set it to the starting hour.
        if (startTime < workdayStartTime) startTime = workdayStartTime;
    
        var timeBeforeEndOfWorkDay = workdayEndTime - startTime;
    
        // If it's after the end of the day, then this time lapse doesn't count.
        if (timeBeforeEndOfWorkDay.TotalSeconds < 0) timeBeforeEndOfWorkDay = new TimeSpan();
        // If start is during a non work day, it doesn't count.
        if (nonWorkDays.Contains(start.DayOfWeek)) timeBeforeEndOfWorkDay = new TimeSpan();
        else if (holidays.Contains(start.Date)) timeBeforeEndOfWorkDay = new TimeSpan();
    
        var endTime = end.TimeOfDay;
    
        // If the end time is after the ending hours, set it to the ending hour.
        if (endTime > workdayEndTime) endTime = workdayEndTime;
    
        var timeAfterStartOfWorkDay = endTime - workdayStartTime;
    
        // If it's before the start of the day, then this time lapse doesn't count.
        if (timeAfterStartOfWorkDay.TotalSeconds < 0) timeAfterStartOfWorkDay = new TimeSpan();
        // If end is during a non work day, it doesn't count.
        if (nonWorkDays.Contains(end.DayOfWeek)) timeAfterStartOfWorkDay = new TimeSpan();
        else if (holidays.Contains(end.Date)) timeAfterStartOfWorkDay = new TimeSpan();
    
        // Easy scenario if the times are during the day day.
        if (start.Date.CompareTo(end.Date) == 0)
        {
            if (nonWorkDays.Contains(start.DayOfWeek)) return new TimeSpan();
            else if (holidays.Contains(start.Date)) return new TimeSpan();
            return endTime - startTime;
        }
        else
        {
            var timeBetween = end - start;
            var daysBetween = (int)Math.Floor(timeBetween.TotalDays);
            var dailyWorkSeconds = (int)Math.Floor((workdayEndTime - workdayStartTime).TotalSeconds);
    
            var businessDaysBetween = 0;
    
            // Now the fun begins with calculating the actual Business days.
            if (daysBetween > 0)
            {
                var nextStartDay = start.AddDays(1).Date;
                var dayBeforeEnd = end.AddDays(-1).Date;
                for (DateTime d = nextStartDay; d <= dayBeforeEnd; d = d.AddDays(1))
                {
                    if (nonWorkDays.Contains(d.DayOfWeek)) continue;
                    else if (holidays.Contains(d.Date)) continue;
                    businessDaysBetween++;
                }
            }
    
            var dailyWorkSecondsToAdd = dailyWorkSeconds * businessDaysBetween;
    
            var output = timeBeforeEndOfWorkDay + timeAfterStartOfWorkDay;
            output = output + new TimeSpan(0, 0, dailyWorkSecondsToAdd);
    
            return output;
        }
    }
    

    And here is test code: Note that you just have to put this function in a class called DateHelper for the test code to work.

    [TestMethod]
    public void TestGetBusinessTimespanBetween()
    {
        var workdayStart = new TimeSpan(8, 0, 0);
        var workdayEnd = new TimeSpan(17, 0, 0);
    
        var holidays = new List<DateTime>()
        {
            new DateTime(2018, 1, 15), // a Monday
            new DateTime(2018, 2, 15) // a Thursday
        };
    
        var testdata = new[]
        {
            new
            {
                expectedMinutes = 0,
                start = new DateTime(2016, 10, 19, 9, 50, 0),
                end = new DateTime(2016, 10, 19, 9, 50, 0)
            },
            new
            {
                expectedMinutes = 10,
                start = new DateTime(2016, 10, 19, 9, 50, 0),
                end = new DateTime(2016, 10, 19, 10, 0, 0)
            },
            new
            {
                expectedMinutes = 5,
                start = new DateTime(2016, 10, 19, 7, 50, 0),
                end = new DateTime(2016, 10, 19, 8, 5, 0)
            },
            new
            {
                expectedMinutes = 5,
                start = new DateTime(2016, 10, 19, 16, 55, 0),
                end = new DateTime(2016, 10, 19, 17, 5, 0)
            },
            new
            {
                expectedMinutes = 15,
                start = new DateTime(2016, 10, 19, 16, 50, 0),
                end = new DateTime(2016, 10, 20, 8, 5, 0)
            },
            new
            {
                expectedMinutes = 10,
                start = new DateTime(2016, 10, 19, 16, 50, 0),
                end = new DateTime(2016, 10, 20, 7, 55, 0)
            },
            new
            {
                expectedMinutes = 5,
                start = new DateTime(2016, 10, 19, 17, 10, 0),
                end = new DateTime(2016, 10, 20, 8, 5, 0)
            },
            new
            {
                expectedMinutes = 0,
                start = new DateTime(2016, 10, 19, 17, 10, 0),
                end = new DateTime(2016, 10, 20, 7, 5, 0)
            },
            new
            {
                expectedMinutes = 545,
                start = new DateTime(2016, 10, 19, 12, 10, 0),
                end = new DateTime(2016, 10, 20, 12, 15, 0)
            },
            // Spanning multiple weekdays
            new
            {
                expectedMinutes = 835,
                start = new DateTime(2016, 10, 19, 12, 10, 0),
                end = new DateTime(2016, 10, 21, 8, 5, 0)
            },
            // Spanning multiple weekdays
            new
            {
                expectedMinutes = 1375,
                start = new DateTime(2016, 10, 18, 12, 10, 0),
                end = new DateTime(2016, 10, 21, 8, 5, 0)
            },
            // Spanning from a Thursday to a Tuesday, 5 mins short of complete day.
            new
            {
                expectedMinutes = 1615,
                start = new DateTime(2016, 10, 20, 12, 10, 0),
                end = new DateTime(2016, 10, 25, 12, 5, 0)
            },
            // Spanning from a Thursday to a Tuesday, 5 mins beyond complete day.
            new
            {
                expectedMinutes = 1625,
                start = new DateTime(2016, 10, 20, 12, 10, 0),
                end = new DateTime(2016, 10, 25, 12, 15, 0)
            },
            // Spanning from a Friday to a Monday, 5 mins beyond complete day.
            new
            {
                expectedMinutes = 545,
                start = new DateTime(2016, 10, 21, 12, 10, 0),
                end = new DateTime(2016, 10, 24, 12, 15, 0)
            },
            // Spanning from a Friday to a Monday, 5 mins short complete day.
            new
            {
                expectedMinutes = 535,
                start = new DateTime(2016, 10, 21, 12, 10, 0),
                end = new DateTime(2016, 10, 24, 12, 5, 0)
            },
            // Spanning from a Saturday to a Monday, 5 mins short complete day.
            new
            {
                expectedMinutes = 245,
                start = new DateTime(2016, 10, 22, 12, 10, 0),
                end = new DateTime(2016, 10, 24, 12, 5, 0)
            },
            // Spanning from a Saturday to a Sunday, 5 mins beyond complete day.
            new
            {
                expectedMinutes = 0,
                start = new DateTime(2016, 10, 22, 12, 10, 0),
                end = new DateTime(2016, 10, 23, 12, 15, 0)
            },
            // Times within the same Saturday.
            new
            {
                expectedMinutes = 0,
                start = new DateTime(2016, 10, 22, 12, 10, 0),
                end = new DateTime(2016, 10, 23, 12, 15, 0)
            },
            // Spanning from a Saturday to the Sunday next week.
            new
            {
                expectedMinutes = 2700,
                start = new DateTime(2016, 10, 22, 12, 10, 0),
                end = new DateTime(2016, 10, 30, 12, 15, 0)
            },
            // Spanning a year.
            new
            {
                expectedMinutes = 143355,
                start = new DateTime(2016, 10, 22, 12, 10, 0),
                end = new DateTime(2017, 10, 30, 12, 15, 0)
            },
            // Spanning a year with 2 holidays.
            new
            {
                expectedMinutes = 142815,
                start = new DateTime(2017, 10, 22, 12, 10, 0),
                end = new DateTime(2018, 10, 30, 12, 15, 0)
            },
        };
    
        foreach (var item in testdata)
        {
            Assert.AreEqual(item.expectedMinutes,
                DateHelper.GetBusinessTimespanBetween(
                    item.start, item.end,
                    workdayStart, workdayEnd,
                    holidays)
                    .TotalMinutes);
        }
    }
    
    0 讨论(0)
  • 2020-11-22 15:40

    I believe this could be a simpler way:

        public int BusinessDaysUntil(DateTime start, DateTime end, params DateTime[] bankHolidays)
        {
            int tld = (int)((end - start).TotalDays) + 1; //including end day
            int not_buss_day = 2 * (tld / 7); //Saturday and Sunday
            int rest = tld % 7; //rest.
    
            if (rest > 0)
            {
                int tmp = (int)start.DayOfWeek - 1 + rest;
                if (tmp == 6 || start.DayOfWeek == DayOfWeek.Sunday) not_buss_day++; else if (tmp > 6) not_buss_day += 2;
            }
    
            foreach (DateTime bankHoliday in bankHolidays)
            {
                DateTime bh = bankHoliday.Date;
                if (!(bh.DayOfWeek == DayOfWeek.Saturday || bh.DayOfWeek == DayOfWeek.Sunday) && (start <= bh && bh <= end))
                {
                    not_buss_day++;
                }
            }
            return tld - not_buss_day;
        }
    
    0 讨论(0)
  • 2020-11-22 15:41

    This solution avoids iteration, works for both +ve and -ve weekday differences and includes a unit test suite to regression against the slower method of counting weekdays. I've also include a concise method to add weekdays also works in the same non-iterative way.

    Unit tests cover a few thousand date combinations in order to exhaustively test all start/end weekday combinations with both small and large date ranges.

    Important: We make the assumption that we are counting days by excluding the start date and including the end date. This is important when counting weekdays as the specific start/end days that you include/exclude affect the result. This also ensures that the difference between two equal days is always zero and that we only include full working days as typically you want the answer to be correct for any time on the current start date (often today) and include the full end date (e.g. a due date).

    NOTE: This code needs an additional adjustment for holidays but in keeping with the above assumption, this code must exclude holidays on the start date.

    Add weekdays:

    private static readonly int[,] _addOffset = 
    {
      // 0  1  2  3  4
        {0, 1, 2, 3, 4}, // Su  0
        {0, 1, 2, 3, 4}, // M   1
        {0, 1, 2, 3, 6}, // Tu  2
        {0, 1, 4, 5, 6}, // W   3
        {0, 1, 4, 5, 6}, // Th  4
        {0, 3, 4, 5, 6}, // F   5
        {0, 2, 3, 4, 5}, // Sa  6
    };
    
    public static DateTime AddWeekdays(this DateTime date, int weekdays)
    {
        int extraDays = weekdays % 5;
        int addDays = weekdays >= 0
            ? (weekdays / 5) * 7 + _addOffset[(int)date.DayOfWeek, extraDays]
            : (weekdays / 5) * 7 - _addOffset[6 - (int)date.DayOfWeek, -extraDays];
        return date.AddDays(addDays);
    }
    

    Compute weekday difference:

    static readonly int[,] _diffOffset = 
    {
      // Su M  Tu W  Th F  Sa
        {0, 1, 2, 3, 4, 5, 5}, // Su
        {4, 0, 1, 2, 3, 4, 4}, // M 
        {3, 4, 0, 1, 2, 3, 3}, // Tu
        {2, 3, 4, 0, 1, 2, 2}, // W 
        {1, 2, 3, 4, 0, 1, 1}, // Th
        {0, 1, 2, 3, 4, 0, 0}, // F 
        {0, 1, 2, 3, 4, 5, 0}, // Sa
    };
    
    public static int GetWeekdaysDiff(this DateTime dtStart, DateTime dtEnd)
    {
        int daysDiff = (int)(dtEnd - dtStart).TotalDays;
        return daysDiff >= 0
            ? 5 * (daysDiff / 7) + _diffOffset[(int) dtStart.DayOfWeek, (int) dtEnd.DayOfWeek]
            : 5 * (daysDiff / 7) - _diffOffset[6 - (int) dtStart.DayOfWeek, 6 - (int) dtEnd.DayOfWeek];
    }
    

    I found that most other solutions on stack overflow were either slow (iterative) or overly complex and many were just plain incorrect. Moral of the story is ... Don't trust it unless you've exhaustively tested it!!

    Unit tests based on NUnit Combinatorial testing and ShouldBe NUnit extension.

    [TestFixture]
    public class DateTimeExtensionsTests
    {
        /// <summary>
        /// Exclude start date, Include end date
        /// </summary>
        /// <param name="dtStart"></param>
        /// <param name="dtEnd"></param>
        /// <returns></returns>
        private IEnumerable<DateTime> GetDateRange(DateTime dtStart, DateTime dtEnd)
        {
            Console.WriteLine(@"dtStart={0:yy-MMM-dd ffffd}, dtEnd={1:yy-MMM-dd ffffd}", dtStart, dtEnd);
    
            TimeSpan diff = dtEnd - dtStart;
            Console.WriteLine(diff);
    
            if (dtStart <= dtEnd)
            {
                for (DateTime dt = dtStart.AddDays(1); dt <= dtEnd; dt = dt.AddDays(1))
                {
                    Console.WriteLine(@"dt={0:yy-MMM-dd ffffd}", dt);
                    yield return dt;
                }
            }
            else
            {
                for (DateTime dt = dtStart.AddDays(-1); dt >= dtEnd; dt = dt.AddDays(-1))
                {
                    Console.WriteLine(@"dt={0:yy-MMM-dd ffffd}", dt);
                    yield return dt;
                }
            }
        }
    
        [Test, Combinatorial]
        public void TestGetWeekdaysDiff(
            [Values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 20, 30)]
            int startDay,
            [Values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 20, 30)]
            int endDay,
            [Values(7)]
            int startMonth,
            [Values(7)]
            int endMonth)
        {
            // Arrange
            DateTime dtStart = new DateTime(2016, startMonth, startDay);
            DateTime dtEnd = new DateTime(2016, endMonth, endDay);
    
            int nDays = GetDateRange(dtStart, dtEnd)
                .Count(dt => dt.DayOfWeek != DayOfWeek.Saturday && dt.DayOfWeek != DayOfWeek.Sunday);
    
            if (dtEnd < dtStart) nDays = -nDays;
    
            Console.WriteLine(@"countBusDays={0}", nDays);
    
            // Act / Assert
            dtStart.GetWeekdaysDiff(dtEnd).ShouldBe(nDays);
        }
    
        [Test, Combinatorial]
        public void TestAddWeekdays(
            [Values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 20, 30)]
            int startDay,
            [Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 20, 30)]
            int weekdays)
        {
            DateTime dtStart = new DateTime(2016, 7, startDay);
            DateTime dtEnd1 = dtStart.AddWeekdays(weekdays);     // ADD
            dtStart.GetWeekdaysDiff(dtEnd1).ShouldBe(weekdays);  
    
            DateTime dtEnd2 = dtStart.AddWeekdays(-weekdays);    // SUBTRACT
            dtStart.GetWeekdaysDiff(dtEnd2).ShouldBe(-weekdays);
        }
    }
    
    0 讨论(0)
提交回复
热议问题