【问题标题】:C# - calculate sla in days, hours and minutes excluding non working hours, weekends and public holidaysC# - 以天、小时和分钟计算 SLA,不包括非工作时间、周末和公共假期
【发布时间】:2019-02-21 08:58:13
【问题描述】:

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

虽然我在可用问题上发现了一些相似之处,但我找不到任何关于如何在 c# 中以天、小时和分钟计算 SLA 的特定问题和回应,不包括公共假期、周末和非工作时间。

例如,我将工单提出的日期时间设置为 21/02/2019 10:00:00 pm,如果我只想添加 n(在本例中为 21)工作小时数,不包括非工作时间,在 C# 中查找该票的 sla 日期时间。

虽然我在仅计算工作时间、周末时实施了一些逻辑,但发现很难排除公共假期。也欣赏比长行函数更好、简单和易于理解的方法(可能使用 linq)。感谢社区提供的任何示例代码。

我从下面的其他 stackoverflow 链接中得到了一个工作解决方案,但这需要更多的改进来简化和解决任何可能的错误,例如如果我们连续放 2 天假期,则无法处理这种情况,然后计算 sla从第 3 天开始,等等。

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

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

【问题讨论】:

  • 您有公共假期的数据源吗?
  • 是的。我几乎没有选择,即带有用户输入的假日日期和描述的假日表。选项 2 是从特定于国家/地区的公共假日 api 接收假日 json 并将其存储在我们的系统中以计算此逻辑。具体来说,基本假期表将像 Id、日期、名称。
  • 我不明白为什么有些人反对这个问题?那些人可以留下一些关于为什么不投票的问题会很好。谢谢
  • 可能是因为您提出了如此广泛的问题,其中涉及更详细的答案,并且通过展示您尝试实施的示例以及您实际遇到的问题来表明您没有努力解决问题。其次,人们实际上给了你 2 个解决你的问题的方法,你甚至都懒得去研究它
  • @Tiago,事实上我已经有了相同的解决方案,因为它更接近我的问题,但这并没有排除假期,还通过重构寻找任何优雅的解决方案。因此,我已经尽我所能编辑了我的问题,并接受了您的努力作为答案,并将其与我的业务逻辑和单元测试相结合。顺便说一句,我不同意这个问题如此广泛,需要更详细的答案。我正在寻找的只是一些基本的逻辑/伪代码也可以。确保考虑更好的方法。

标签: c# .net linq


【解决方案1】:

实际上,我花了最后一个小时来实现这个解决方案,它结合了另一个 stackoverflow 问题 (Add hours to datetime but exclude weekends and should be between working hours) 的 aswner,它计算到日期的工作时间 + 一个根据指定的国家/地区验证日期是否为假期的块. 首先安装金块

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 包支持的任何其他国家/地区代码。

希望对你有帮助!这对我来说是一个有趣的挑战,但对未来的实施很有用:)

【讨论】:

  • 感谢您的努力和帮助。事实上,我从另一个 stackoverflow 链接中得到了相同的解决方案,并进行了一些改进,因为这并不排除公共假期。我正在处理您的假期,不包括集成到我的业务逻辑中的解决方案。还在寻找使用不同日期选项进行简化和单元测试的所有可能性。
【解决方案2】:

根据我的经验,这是一个非常容易出错的任务。如果您在整个小时或几天内工作,我建议您仅列举每个并保留总共合格的。

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

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
}

【讨论】:

  • 感谢克里斯蒂安,我正在完善我所拥有的解决方案。会调查这个图书馆是否有帮助。
【解决方案3】:

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

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

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

我总是将 db 中的日期存储为 UTC,因此请注意您需要将 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;
    }
}

【讨论】:

  • 喜欢带有 UTC 转换的干净代码。。谢谢 :)
  • 很难将此代码转换为 java,这里等效的时间跨度是多少
【解决方案4】:

幸运的是,我需要相同的功能,并且我昨天构建了它并在 NuGet.org 上将其作为 NuGet 共享

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

您可以定义包含多个班次、假期的每周工作计划。结果将以时间跨度的形式出现,您甚至可以在工作时间内提取秒级的 SLA。

【讨论】:

    猜你喜欢
    • 2016-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-12
    • 1970-01-01
    • 2012-10-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多