C#-以天,小时和分钟为单位计算sla,不包括非工作时间,周末和公共假期

时间:2019-02-21 08:58:13

标签: c# .net linq

我正在搜索stackoverflow,以找到完全匹配的问题以及对类似问题的响应,以在C#中解决。

尽管我在可用的问题上发现了一些相似之处,但在公共假期,周末和非工作时间中,如何在c#中以天,小时和分钟为单位计算sla并没有发现任何特定的问题和答案。

例如,我将票证的日期时间提高为21/02/2019 10:00:00 pm,如果我只想添加n(在本示例中为21)工作时间(不包括非工作时间),周末和公共假期在C#中查找该票证的sla日期时间。

尽管我在仅计算工作时间,周末时实现了一些逻辑,但很难排除公众假期。还要欣赏比长行函数更好,更简单且易于理解的方法(可能使用linq)。感谢社区中的任何示例代码。

我从下面的其他stackoverflow链接中得到了一个改进的工作解决方案,但是如果我们连续获得2天的假期,则需要进一步完善以简化和解决这种情况下无法解决的所有错误,然后计算sla从第三天开始,等等。

到目前为止,我得到的解决方案是:

public virtual DateTime AddWithinWorkingHours(DateTime start, TimeSpan offset)
    {
        //Get publicholidaysList from holiday table to not to include in working hour calculation
        var holidaysList = _holidayManager.GetHolidays().Result;

        // Don't start counting hours until start time is during working hours
        if (start.TimeOfDay.TotalHours > StartHour + HoursPerDay)
            start = start.Date.AddDays(1).AddHours(StartHour);
        if (start.TimeOfDay.TotalHours < StartHour)
            start = start.Date.AddHours(StartHour);
        if (start.DayOfWeek == DayOfWeek.Saturday)
            start.AddDays(2);
        //if it is a Sunday or holiday date, skip that date in workinghour calc
        else if (start.DayOfWeek == DayOfWeek.Sunday || holidaysList.Exists(hd=>hd.Date == start.Date))
            start.AddDays(1);
        // Calculate how much working time already passed on the first day
        TimeSpan firstDayOffset = start.TimeOfDay.Subtract(TimeSpan.FromHours(StartHour));
        // Calculate number of whole days to add
        int wholeDays = (int)(offset.Add(firstDayOffset).TotalHours / HoursPerDay);
        // How many hours off the specified offset does this many whole days consume?
        TimeSpan wholeDaysHours = TimeSpan.FromHours(wholeDays * HoursPerDay);
        // Calculate the final time of day based on the number of whole days spanned and the specified offset
        TimeSpan remainder = offset - wholeDaysHours;
        // How far into the week is the starting date?
        int weekOffset = ((int)(start.DayOfWeek + 7) - (int)DayOfWeek.Monday) % 7;
        // How many weekends are spanned?
        int weekends = (int)((wholeDays + weekOffset) / 5);
        // Calculate the final result using all the above calculated values
        return start.AddDays(wholeDays + weekends * 2).Add(remainder);
    } 

3 个答案:

答案 0 :(得分:0)

我实际上已经花费了最后一个小时来实施此解决方案,该解决方案结合了另一个stackoverflow问题(Add hours to datetime but exclude weekends and should be between working hours)的计算方法,该问题计算了一个日期的工作时间+一个块金,该块验证日期是否为假期,具体取决于哪个指定的国家/地区。 首先安装金块

PM> install-package Nager.Date

然后,我创建了3种方法来实现您的功能,但它很简单,您可以对其进行优化,以使用CountryCode以及工作日的工作时间以及何时开始,但出于示例目的,我对其进行了硬编码:

        private static DateTime AddWithinWorkingHours(DateTime start, TimeSpan offset)
        {
            const int hoursPerDay = 8;
            const int startHour = 9;

            // Don't start counting hours until start time is during working hours
            if (start.TimeOfDay.TotalHours > startHour + hoursPerDay)
                start = start.Date.AddDays(1).AddHours(startHour);
            if (start.TimeOfDay.TotalHours < startHour)
                start = start.Date.AddHours(startHour);

            start = CheckTillNoLongerHoliday(start);

            if (start.DayOfWeek == DayOfWeek.Saturday)
                start = start.AddDays(2);
            else if (start.DayOfWeek == DayOfWeek.Sunday)
                start = start.AddDays(1);

            //Saving this proccessed date to check later if there are more holidays
            var dateAfterArranges = start;

            // Calculate how much working time already passed on the first day
            TimeSpan firstDayOffset = start.TimeOfDay.Subtract(TimeSpan.FromHours(startHour));

            // Calculate number of whole days to add
            int wholeDays = (int)(offset.Add(firstDayOffset).TotalHours / hoursPerDay);

            // How many hours off the specified offset does this many whole days consume?
            TimeSpan wholeDaysHours = TimeSpan.FromHours(wholeDays * hoursPerDay);

            // Calculate the final time of day based on the number of whole days spanned and the specified offset
            TimeSpan remainder = offset - wholeDaysHours;

            // How far into the week is the starting date?
            int weekOffset = ((int)(start.DayOfWeek + 7) - (int)DayOfWeek.Monday) % 7;

            // How many weekends are spanned?
            int weekends = (int)((wholeDays + weekOffset) / 5);

            //Get the final date without the holidays
            start = start.AddDays(wholeDays + weekends * 2).Add(remainder);

            //Check again if in this timeSpan there were any more holidays
            return InPeriodCheckHolidaysOnWorkingDays(dateAfterArranges, start);
        }


        private static DateTime CheckTillNoLongerHoliday(DateTime date)
        {
            if (DateSystem.IsPublicHoliday(date, CountryCode.PT) && !DateSystem.IsWeekend(date, CountryCode.PT))
            {
                date = date.AddDays(1);
                date = CheckTillNoLongerHoliday(date);
            }

            return date;
        }


        private static DateTime InPeriodCheckHolidaysOnWorkingDays(DateTime start, DateTime end)
        {
            var publicHolidays = DateSystem.GetPublicHoliday(2019, CountryCode.PT);

            var holidaysSpent = publicHolidays.Where(x => x.Date.Date >= start.Date && x.Date.Date < end.Date);
            foreach (var holiday in holidaysSpent)
            {
                if (!DateSystem.IsWeekend(holiday.Date, CountryCode.PT))
                {
                    end = end.AddDays(1);
                    if (DateSystem.IsWeekend(end, CountryCode.PT))
                    {
                        end = end.AddDays(2);
                    }
                }
            }

            return end;
        }

我实现的是3种方法:AddWithinWorkingHours是所有方法的主要方法,它是由用户在我提到的那个链接上完成的(也要给他功劳)(基本上也要给他功劳),它需要一个DateTime开始日期(在您的示例中是工单增加的时间)和一个TimeSpan,您可以传递工作时间数。接下来的2种方法是考虑“国家假日”的方法,在该示例中您注意到了,我使用了葡萄牙语假日,但是您可以使用Nager.Date nuget程序包支持的任何其他国家/地区代码。

希望它对您有帮助!这对我来说是一个有趣的挑战,但对将来的实现很有用:)

答案 1 :(得分:0)

根据我的经验,这是一个非常容易出错的任务。如果您整小时或全天工作,我建议您逐一列举并保持合格的时间。

但是,如果需要精确度,最好使用库。

Tiago链接到的答案之一中引用的库似乎完全符合您的要求:

https://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET

public void CalendarDateAddSample()
{
  CalendarDateAdd calendarDateAdd = new CalendarDateAdd();
  // weekdays
  calendarDateAdd.AddWorkingWeekDays();
  // holidays
  calendarDateAdd.ExcludePeriods.Add( new Day( 2011, 4, 5, calendarDateAdd.Calendar ) );
  // working hours
  calendarDateAdd.WorkingHours.Add( new HourRange( new Time( 08, 30 ), new Time( 12 ) ) );
  calendarDateAdd.WorkingHours.Add( new HourRange( new Time( 13, 30 ), new Time( 18 ) ) );

  DateTime start = new DateTime( 2011, 4, 1, 9, 0, 0 );
  TimeSpan offset = new TimeSpan( 22, 0, 0 ); // 22 hours

  DateTime? end = calendarDateAdd.Add( start, offset );

  Console.WriteLine( "start: {0}", start );
  // > start: 01.04.2011 09:00:00
  Console.WriteLine( "offset: {0}", offset );
  // > offset: 22:00:00
  Console.WriteLine( "end: {0}", end );
  // > end: 06.04.2011 16:30:00
}

答案 2 :(得分:0)

这是我计算SLA的解决方案。比我所见的一些过于复杂的文章更容易阅读。代码应始终易于他人维护。

它仅计算工作时间内的时间(工作日的开始和结束值存储在db中,因此可以配置)。它考虑了周六和周日以及任何公共假日(来自内存中的缓存列表)。

    public DateTime? CalculateSLADueDate(DateTime slaStartDateUTC, double slaDays, TimeSpan workdayStartUTC, TimeSpan workdayEndUTC)
    {
        if ((slaDays < 0)
        || (workdayStartUTC > workdayEndUTC))
        {
            return null;
        }

        var dueDate = slaStartDateUTC;
        var tsWorkdayHours = (workdayEndUTC - workdayStartUTC);
        var tsSlaCount = TimeSpan.FromHours(slaDays * ((workdayEndUTC - workdayStartUTC)).TotalHours);

        //get list of public holidays from in-memory cache
        var blPublicHoliday = new PublicHoliday();
        IList<BusObj.PublicHoliday> publicHolidays = blPublicHoliday.SelectAll();

        do
        {
            if ((dueDate.DayOfWeek == DayOfWeek.Saturday)
            || (dueDate.DayOfWeek == DayOfWeek.Sunday)
            || publicHolidays.Any(x => x.HolidayDate == dueDate.Date)
            || ((dueDate.TimeOfDay >= workdayEndUTC) && (dueDate.TimeOfDay < workdayStartUTC)))
            {
                //jump to start of next day
                dueDate = dueDate.AddDays(1);
                dueDate = new DateTime(dueDate.Year, dueDate.Month, dueDate.Day, workdayStartUTC.Hours, workdayStartUTC.Minutes, workdayStartUTC.Seconds);
            }
            else if ((dueDate.TimeOfDay == workdayStartUTC) && (tsSlaCount >= tsWorkdayHours))
            {
                //add a whole working day
                dueDate = dueDate.AddDays(1);
                tsSlaCount = tsSlaCount.Subtract(tsWorkdayHours);
            }
            else if (dueDate.TimeOfDay == workdayStartUTC)
            {
                //end day - add remainder of time for final work day
                dueDate = dueDate.Add(tsSlaCount);
                tsSlaCount = tsSlaCount.Subtract(tsSlaCount);
            }
            else
            {
                if(workdayEndUTC > dueDate.TimeOfDay)
                {
                    //start day and still in business hours - add rest of today
                    tsSlaCount = tsSlaCount.Subtract(workdayEndUTC - dueDate.TimeOfDay);
                    dueDate = dueDate.Add(workdayEndUTC - dueDate.TimeOfDay);
                }

                if (tsSlaCount.Ticks > 0)
                {
                    //if theres more to process - jump to start of next day
                    dueDate = dueDate.AddDays(1);
                    dueDate = new DateTime(dueDate.Year, dueDate.Month, dueDate.Day, workdayStartUTC.Hours, workdayStartUTC.Minutes, workdayStartUTC.Seconds);
                }
            }
        }
        while (tsSlaCount.Ticks > 0);

        return dueDate;
    }

我总是将日期以UTC格式存储在db中,因此请注意,您需要将TimeSpan参数转换为UTC。感谢PeterJ的TimeSpan扩展(我在其中添加了“ this”):

public static class DatetimeExtensionMethod
{
    public static TimeSpan LocalTimeSpanToUTC(this TimeSpan ts)
    {
        DateTime dt = DateTime.Now.Date.Add(ts);
        DateTime dtUtc = dt.ToUniversalTime();
        TimeSpan tsUtc = dtUtc.TimeOfDay;
        return tsUtc;
    }

    public static TimeSpan UTCTimeSpanToLocal(this TimeSpan tsUtc)
    {
        DateTime dtUtc = DateTime.UtcNow.Date.Add(tsUtc);
        DateTime dt = dtUtc.ToLocalTime();
        TimeSpan ts = dt.TimeOfDay;
        return ts;
    }
}