简体   繁体   English

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

[英]C# - calculate sla in days, hours and minutes excluding non working hours, weekends and public holidays

I was searching stackoverflow to find the exact match question and response on similar problem to solve in C#.我正在搜索 stackoverflow 以找到在 C# 中解决的类似问题的精确匹配问题和响应。

Though I found couple of similarities on the questions available, I would not find any particular question and response on how to calculate the sla in days, hours and minutes in c# excluding the public holidays, weekends and non working hours.尽管我在可用问题上发现了一些相似之处,但我找不到关于如何在 c# 中以天、小时和分钟为单位计算 SLA 的任何特定问题和回应,不包括公共假期、周末和非工作时间。

For example, I've the ticket raised datetime as 21/02/2019 10:00:00 pm and if I would like to add only n (say 21 in this example) number of working hours excluding non working hours, weekends and public holidays to find the sla datetime of that ticket in C#.例如,我将票的日期时间提高为 21/02/2019 10:00:00 pm,如果我只想添加 n(在此示例中为 21)工作小时数,不包括非工作时间、周末和公共假期在 C# 中查找该票的 sla 日期时间。

Though I've some logics implemented on calculating only working hours, weekends, but finding hard to exclude the public holidays.虽然我已经实现了一些仅计算工作时间和周末的逻辑,但发现很难排除公共假期。 Also appreciate the better, simple and understandable way of doing (using linq probably) than long lines of functions.还要欣赏比长行函数更好、更简单和易于理解的方法(可能使用 linq)。 Appreciate any sample code from the community.感谢来自社区的任何示例代码。

I've got a working solution refined from other stackoverflow link as below, but this needs more refinement towards simplifying and resolving any possibilities of bugs like this scenario didn't handle if we get 2 days of holiday continuously, then calculate sla from the 3rd day, etc.我有一个从其他 stackoverflow 链接中提炼出来的工作解决方案,如下所示,但这需要更多的改进来简化和解决这种情况下的错误的任何可能性,如果我们连续获得 2 天的假期,则无法处理这种情况,然后从第 3 天开始计算 sla天等。

The solution I've got so far is:到目前为止我得到的解决方案是:

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

I have actually spent the last hour implementing this solution which combines an aswner from another stackoverflow question ( Add hours to datetime but exclude weekends and should be between working hours ) that calculates to a date the working hours + a nugget that validates if a date its an holiday depending on which country specified.我实际上花了最后一个小时来实现这个解决方案,它结合了来自另一个计算器问题的 aswner( 将时间添加到日期时间但不包括周末并且应该在工作时间之间)计算到某个日期的工作时间 + 一个验证日期是否为它的金块假期取决于指定的国家/地区。 First install the nugget首先安装金块

PM> install-package Nager.Date

Then I created 3 methods to make your functionality but its simple and you can optimize it to take in CountryCode and how many hours are in a working day and when does it start , but I made it hard coded just for example purposes:然后我创建了 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;
        }

What I have implemented are 3 methods: AddWithinWorkingHours it's the main method that as all the base functionalities and was made by the user on that link I mentioned (Go give him credit as well) basicly it takes in a DateTime of the start date ( in your example its the ticket raised time) and a TimeSpan which you can pass number of working hours.我已经实现了 3 种方法: AddWithinWorkingHours 它是主要方法,作为所有基本功能,并且由用户在我提到的那个链接上制作(也给他信用)基本上它需要开始日期的 DateTime(在你的例子是它的票提出时间)和一个你可以通过工作小时数的时间跨度。 Then the next 2 methods are the ones taking in the account for the Country Holidays, as you can notice it in that example i used Portuguese holidays but you can use any other country code supported by the Nager.Date nuget package.然后接下来的 2 种方法是考虑国家假期的方法,正如您在该示例中所注意到的那样,我使用了葡萄牙假期,但您可以使用 Nager.Date nuget 包支持的任何其他国家/地区代码。

I hope it really helps you!我希望它真的对你有帮助! This was a fun challenge to me but useful for future implementations :)这对我来说是一个有趣的挑战,但对未来的实现很有用:)

This is a really error prone task in my experience.根据我的经验,这是一项非常容易出错的任务。 If you were working in whole hours or days I would suggest just enumerating through each and keeping a total of qualifying ones.如果您在整个小时或几天内工作,我建议您只枚举每个并保留所有合格的。

However if you need minute precision, better to use a library.但是,如果您需要精确度,最好使用库。

The library referenced in one of the answers that Tiago links to seems to do exactly what you want: Tiago 链接到的其中一个答案中引用的库似乎完全符合您的要求:

https://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET 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
}

Here is my solution for calculating SLAs.这是我计算 SLA 的解决方案。 Easier to read than some over-complicated ones I've seen.比我见过的一些过于复杂的更容易阅读。 Code should always be easily maintainable by someone else.代码应该始终易于其他人维护。

It only counts time within business hours (workday start and end values are stored in db and hence configurable).它只计算工作时间内的时间(工作日开始和结束值存储在 db 中,因此可配置)。 It takes account of Saturday and Sunday and any public holidays (from an in-memory cached list).它考虑了周六和周日以及任何公共假期(来自内存缓存列表)。

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

I always store dates in db as UTC, so be wary you need to convert TimeSpan parameters to UTC.我总是将 db 中的日期存储为 UTC,因此请注意您需要将 TimeSpan 参数转换为 UTC。 Thanks to PeterJ for his TimeSpan extensions (I added the 'this' to them):感谢 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;
    }
}

Luckily for you, I needed the same functionality and I have built it yesterday and shared it as a NuGet on NuGet.org幸运的是,我需要相同的功能,我昨天已经构建并在 NuGet.org 上将其作为 NuGet 共享

https://www.nuget.org/packages/WorkTimeCalculator https://www.nuget.org/packages/WorkTimeCalculator

You can define a weekly working schedule with multiple shifts, holidays.您可以定义具有多个班次、假期的每周工作计划。 The result will be in the form of time span, you can even extract seconds of SLA in work time.结果将以时间跨度的形式出现,您甚至可以提取工作时间中的 SLA 秒数。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM