Вычисление "рабочего времени" с помощью TimePeriod.NET CalendarPeriodCollector дает неожиданные результаты

Я пытаюсь рассчитать срок действия соглашения об уровне обслуживания, и в то же время мне также необходимо вернуться к расчету соглашения об уровне обслуживания в другом направлении.

Я боролся с вычислениями за "рабочее время" (т.е. время, в течение которого работа возможна в течение нескольких дней), и решил использовать стороннюю библиотеку под названием TimePeriodLibrary.NET для задачи. Мне нужно сделать две вещи:

  • При запуске DateTime и TimeSpan вы должны получить DateTime, когда дата соглашения о уровне обслуживания является обязательной (с учетом даты).
  • Учитывая начало DateTime и конец DateTime, вы должны получить TimeSpan того, как долго должно быть достигнуто соглашение об уровне обслуживания.

Весь исходный код (тестовый проект находится на GitHub). У меня есть класс ServiceLevelManager, который выполняет всю работу. Он принимает список WorkDays и HolidayPeriods, чтобы определить, какие часы доступны для работы. Класс CalendarPeriodCollector дает неожиданные результаты. Ожидания, которые работают при определении даты выполнения с временного интервала, не вычисляются правильно, когда я возвращаю их.

Может ли кто-нибудь увидеть, что я делаю что-то неправильно, или у библиотеки есть ошибка?

namespace ServicePlanner
{
    using System;
    using System.Collections.Generic;
    using Itenso.TimePeriod;

    public class ServicePlannerManager
    {
        public ServicePlannerManager(IEnumerable<WorkDay> workDays, IEnumerable<HolidayPeriod> holidays)
        {
            this.WorkDays = workDays;
            this.Holidays = holidays;
        }

        public IEnumerable<WorkDay> WorkDays { get; set; }

        public IEnumerable<HolidayPeriod> Holidays { get; set; }

        public TimeSpan GetRemainingWorkingTime(DateTime start, DateTime dueDate)
        {
            var filter = new CalendarPeriodCollectorFilter();
            foreach (var dayOfWeek in this.WorkDays)
            {
                filter.CollectingDayHours.Add(new DayHourRange(dayOfWeek.DayOfWeek, new Time(dayOfWeek.StartTime), new Time(dayOfWeek.EndTime)));
            }

            foreach (var holiday in this.Holidays)
            {
                filter.ExcludePeriods.Add(new TimeBlock(holiday.StartTime, holiday.EndTime));
            }

            var range = new CalendarTimeRange(start, dueDate);
            var collector = new CalendarPeriodCollector(filter, range);
            collector.CollectHours();

            var duration = collector.Periods.GetTotalDuration(new TimeZoneDurationProvider(TimeZoneInfo.FindSystemTimeZoneById("UTC")));
            return duration;
            //var rounded = Math.Round(duration.TotalMinutes, MidpointRounding.AwayFromZero);
            //return TimeSpan.FromMinutes(rounded);
        }
    }
}

Идентификаторы модулей, которые не выполняются, извлекаются ниже:

[TestFixture]
public class ServicePlannerManagerTest
{
        [Test, TestCaseSource("LocalSource")]
    public void GetRemainingWorkingTimeWithHolidayShouldOnlyEnumerateWorkingTime(DateTime startTime, TimeSpan workingHours, DateTime expectedDueDate, string expectation)
    {
        // Arrange
        var workDays = new List<WorkDay>
        { 
            new WorkDay(DayOfWeek.Monday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
            new WorkDay(DayOfWeek.Tuesday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
            new WorkDay(DayOfWeek.Wednesday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
            new WorkDay(DayOfWeek.Thursday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
            new WorkDay(DayOfWeek.Friday, new DateTime(1, 1, 1, 9, 0, 0), new DateTime(1, 1, 1, 17, 0, 0)),
        };
        var holidayPeriods = new List<HolidayPeriod>
        { 
            new HolidayPeriod(new DateTime(2015, 9, 15, 00, 0, 0), new DateTime(2015, 9, 16, 0, 0, 0))
        };
        var service = new ServicePlannerManager(workDays, holidayPeriods);

        // Act
        var result = service.GetRemainingWorkingTime(startTime, expectedDueDate);

        // Assert - 
        Assert.AreEqual(workingHours.TotalHours, result.TotalHours, expectation);
    }

    protected IEnumerable LocalSource()
    {
        yield return
            new TestCaseData(
                new DateTime(2015, 9, 14, 9, 0, 0),
                new TimeSpan(23, 0, 0),
                new DateTime(2015, 9, 17, 16, 0, 0),
                    "5. Expected 23 hours of working time to end on the 17/09/2015 16:00. Monday to Thursday evening. Just short of 3 full working days by one hour. Tuesday is holiday.");
    }
}

Результат этого теста

5. Expected 23 hours of working time to end on the 17/09/2015 16:00. Monday to Thursday evening. Just short of 3 full working days by one hour. Tuesday is holiday.

Expected: 23.0d
But was:  15.999999999944444d

Я хочу знать, неправильно ли я использую сборщик или если у сборщика есть ошибка.

Ответы

Ответ 1

Это выглядит как отличная библиотека для решения знакомой проблемы.

Лучше всего сделать вывод о периодах в коллекции периодов, чтобы помочь вам отладить проблему.

Я переписал ваш тест, чтобы использовать базовые типы в примерах из своей документации:

        [Test, TestCaseSource("LocalSource")]
    public void SO_GetRemainingWorkingTimeWithHolidayShouldOnlyEnumerateWorkingTime(DateTime startTime,
        TimeSpan workingHours, DateTime expectedDueDate, string expectation)
    {
        CalendarPeriodCollectorFilter filter = new CalendarPeriodCollectorFilter();
        filter.Months.Add(YearMonth.September); // only Januaries
        filter.WeekDays.Add(DayOfWeek.Monday); // 
        filter.WeekDays.Add(DayOfWeek.Tuesday); // 
        filter.WeekDays.Add(DayOfWeek.Wednesday); // 
        filter.WeekDays.Add(DayOfWeek.Thursday); // 
        filter.WeekDays.Add(DayOfWeek.Friday); // 
        filter.CollectingHours.Add(new HourRange(9, 17)); // working hours

        CalendarTimeRange testPeriod = new CalendarTimeRange(startTime, expectedDueDate);//new DateTime(2015, 9, 14, 9, 0, 0), new DateTime(2015, 9, 17, 18, 0, 0));
        Console.WriteLine("Calendar period collector of period: " + testPeriod);

        filter.ExcludePeriods.Add(new TimeBlock(new DateTime(2015, 9, 15, 00, 0, 0), new DateTime(2015, 9, 16, 0, 0, 0)));

        CalendarPeriodCollector collector = new CalendarPeriodCollector(filter, testPeriod);
        collector.CollectHours();

        foreach (ITimePeriod period in collector.Periods)
        {
            Console.WriteLine("Period: " + period); // THIS WILL HELP A LOT!
        }
        var result = collector.Periods.GetTotalDuration(new TimeZoneDurationProvider(TimeZoneInfo.FindSystemTimeZoneById("UTC")));

        Console.WriteLine(result);
            //
    }

Это приводит к:

Calendar period collector of period: 14/09/2015 09:00:00 - 17/09/2015 15:59:59 | 3.06:59
Period: 14/09/2015 09:00:00 - 14/09/2015 16:59:59 | 0.07:59
Period: 16/09/2015 09:00:00 - 16/09/2015 16:59:59 | 0.07:59
15:59:59.9999998

Итак, я заметил, что последний период отсутствует.

Если вы измените время окончания вашего периода с 4 вечера до 6 вечера (и, следовательно, ожидайте дополнительный час = 24), он просто пройдет. (вам также понадобится округлить результат)

Итак, похоже, что периоды должны быть полностью покрыты общей продолжительностью, частичное покрытие не учитывается. Вы можете изменить параметры библиотеки, или вы можете добавить каждый час рабочего дня в качестве отдельного CollectingHours (hacky).

Надеемся, что вы приблизитесь к ответу, который вам нужен!