Библиотека расчёта периодов времени для платформы .NET

Исходный код проекта на CodeProject: Скачать код.
Исходный код проекта на box.com: Скачать код.
Ссылка на оригинал статьи: автор Jani Giannoudis.

Ссылки на родственные заметки:
Что же всё-таки не так со структурой DateTime?,
Маленькие чудеса C#/.NET – структура DateTimeOffset

Вступление

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

Необходимая функциональность затрагивала следующие области:

1. Поддержку индивидуальных периодов времени
2. Работу с календарём периодов внутри календарного года
3. Работу с календарём периодов, выходящим за рамки календарного года (фискальный год или школьный учебный год)

Такие расчёты должны были быть доступны как для серверных компонент (веб-сервисы и задачи), так и для "толстых" клиентов (Silverlight).

Анализ ситуации привёл меня к выводу, что ни компоненты .NET Framework (чего я не ожидал), ни другие доступные инструменты не соответствуют всем моим требованиям. А поскольку я уже сталкивался с подобными проблемами в прежних проектах, то и решил разработать для этих целей библиотеку.

Несколько циклов разработки привели к созданию библиотеки Time Period, доступной ныне для таких сред исполнения:

1. .NET Framework версии 2 и выше
2. .NET Framework для Silverlight с версии 4
3. .NET Framework для Windows Phone с версии 7

С целью демонстрации части функциональности библиотеки, я сделал приложение на Silverlight, назвал его Calendar Period Collector и разместил по адресу http://www.cpc.itenso.com/. Приложение демонстрирует поиск календарных периодов.

Периоды времени

.NET Framework предлагает довольно обширные базовые классы DateTime и TimeSpan для базовых операций со временем. Библиотека Time Period расширяет возможности .NET Framework путём добавления нескольких классов для обработки периодов времени. Такие периоды обычно характеризуются началом, продолжительностью и концом:



По определению, начало всегда происходит перед концом. Старт периода считается неопределённым, если он равен минимально возможному значению (DateTime.MinValue). Аналогично, конец периода считается неопределенным, если он равен максимально возможному значению (DateTime.MaxValue).

Реализация этих временнЫх периодов основана на интерфейсе ITimePeriod и расширена специализированными интерфейсами ITimeRange, ITimeBlock и ITimeInterval:



Интерфейс ITimePeriod содержит информацию и операции для обработки периодов без определения способов, с помощью которых высчитываются определённые свойства:
  • Start, End и Duration временнОго периода
  • HasStart равно true, если определено свойство Start
  • HasEnd равно true, если определено свойство End
  • IsAnytime равно true, если ни свойство Start ни свойство End не определены
  • IsMoment равно true, если свойства Start и End содержат идентичные значения
  • IsReadOnly равно true, если временнОй период неизмЕнен (использование таких периодов см. ниже)

Отношение двух периодов описано перечислением PeriodRelation:



Такие методы как IsSamePeriod, HasInside, OverlapsWith и IntersectsWith доступны для удобства получения часто используемых вариантов подобных отношений между периодами.

Диапазон времени TimeRange

TimeRange, реализуя интерфейс ITimeRange определяет период времени путём установки свойств Start и End; продолжительность периода рассчитывается так:



Объект типа TimeRange может быть создан с помощью передачи переменных, содержащих Start/End, Start/Duration или Duration/End. При необходиости можно хронологически отсортировать Start и End.

Доступно множество операций, предназначенных для модификации таких периодов времени (Оранжевый = новый экземпляр):



В следующем примере показано использование TimeRange:

Код
  1. // --------
  2. public void TimeRangeSample()
  3. {
  4.     // --- time range 1 ---
  5.     TimeRange timeRange1 = new TimeRange(
  6.       new DateTime(2011, 2, 22, 14, 0, 0),
  7.       new DateTime(2011, 2, 22, 18, 0, 0));
  8.     Console.WriteLine("TimeRange1: " + timeRange1);
  9.     // > TimeRange1: 22.02.2011 14:00:00 - 18:00:00 | 04:00:00
  10.  
  11.     // --- time range 2 ---
  12.     TimeRange timeRange2 = new TimeRange(
  13.       new DateTime(2011, 2, 22, 15, 0, 0),
  14.       new TimeSpan(2, 0, 0));
  15.     Console.WriteLine("TimeRange2: " + timeRange2);
  16.     // > TimeRange2: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
  17.  
  18.     // --- time range 3 ---
  19.     TimeRange timeRange3 = new TimeRange(
  20.       new DateTime(2011, 2, 22, 16, 0, 0),
  21.       new DateTime(2011, 2, 22, 21, 0, 0));
  22.     Console.WriteLine("TimeRange3: " + timeRange3);
  23.     // > TimeRange3: 22.02.2011 16:00:00 - 21:00:00 | 05:00:00
  24.  
  25.     // --- отношение ---
  26.     Console.WriteLine("TimeRange1.GetRelation( TimeRange2 ): " +
  27.                        timeRange1.GetRelation(timeRange2));
  28.     // > TimeRange1.GetRelation( TimeRange2 ): Enclosing
  29.     Console.WriteLine("TimeRange1.GetRelation( TimeRange3 ): " +
  30.                        timeRange1.GetRelation(timeRange3));
  31.     // > TimeRange1.GetRelation( TimeRange3 ): EndInside
  32.     Console.WriteLine("TimeRange3.GetRelation( TimeRange2 ): " +
  33.                        timeRange3.GetRelation(timeRange2));
  34.     // > TimeRange3.GetRelation( TimeRange2 ): StartInside
  35.  
  36.     // --- пересечение ---
  37.     Console.WriteLine("TimeRange1.GetIntersection( TimeRange2 ): " +
  38.                        timeRange1.GetIntersection(timeRange2));
  39.     // > TimeRange1.GetIntersection( TimeRange2 ):
  40.     //             22.02.2011 15:00:00 - 17:00:00 | 02:00:00
  41.     Console.WriteLine("TimeRange1.GetIntersection( TimeRange3 ): " +
  42.                        timeRange1.GetIntersection(timeRange3));
  43.     // > TimeRange1.GetIntersection( TimeRange3 ):
  44.     //             22.02.2011 16:00:00 - 18:00:00 | 02:00:00
  45.     Console.WriteLine("TimeRange3.GetIntersection( TimeRange2 ): " +
  46.                        timeRange3.GetIntersection(timeRange2));
  47.     // > TimeRange3.GetIntersection( TimeRange2 ):
  48.     //             22.02.2011 16:00:00 - 17:00:00 | 01:00:00
  49. } // TimeRangeSample


В следующем примере проводится проверка на вхождение времени бронирования в рабочие часы дня:

Код
  1. // --------
  2. public bool IsValidReservation(DateTime start, DateTime end)
  3. {
  4.     if (!TimeCompare.IsSameDay(start, end))
  5.     {
  6.         return false;  // бронирование нескольких дней
  7.     }
  8.  
  9.     TimeRange workingHours =
  10.       new TimeRange(TimeTrim.Hour(start, 8), TimeTrim.Hour(start, 18));
  11.     return workingHours.HasInside(new TimeRange(start, end));
  12. } // IsValidReservation


Блок времени

TimeBlock реализует интерфейс ITimeBlock и определяет период времени с помощью свойств Start и Duration; свойство End вычисляется:



Как и в случае с TimeRange, экземпляр TimeBlock можно создать с помощью задания Start/End, Start/Duration или Duration/End. Также Start и End при необходимости будут автоматически отсортированы.

А вот операции, доступные для модификации блока времени (Оранжевый = новый экземпляр):



Далее - пример использования TimeBlock:

Код
  1. // --------
  2. public void TimeBlockSample()
  3. {
  4.     // --- time block ---
  5.     TimeBlock timeBlock = new TimeBlock(
  6.       new DateTime(2011, 2, 22, 11, 0, 0),
  7.       new TimeSpan(2, 0, 0));
  8.     Console.WriteLine("TimeBlock: " + timeBlock);
  9.     // > TimeBlock: 22.02.2011 11:00:00 - 13:00:00 | 02:00:00
  10.  
  11.     // --- модификация ---
  12.     timeBlock.Start = new DateTime(2011, 2, 22, 15, 0, 0);
  13.     Console.WriteLine("TimeBlock.Start: " + timeBlock);
  14.     // > TimeBlock.Start: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
  15.     timeBlock.Move(new TimeSpan(1, 0, 0));
  16.     Console.WriteLine("TimeBlock.Move(1 hour): " + timeBlock);
  17.     // > TimeBlock.Move(1 hour): 22.02.2011 16:00:00 - 18:00:00 | 02:00:00
  18.  
  19.     // --- предыдущий/следующий ---
  20.     Console.WriteLine("TimeBlock.GetPreviousPeriod(): " +
  21.                        timeBlock.GetPreviousPeriod());
  22.     // > TimeBlock.GetPreviousPeriod(): 22.02.2011 14:00:00 - 16:00:00 | 02:00:00
  23.     Console.WriteLine("TimeBlock.GetNextPeriod(): " + timeBlock.GetNextPeriod());
  24.     // > TimeBlock.GetNextPeriod(): 22.02.2011 18:00:00 - 20:00:00 | 02:00:00
  25.     Console.WriteLine("TimeBlock.GetNextPeriod(+1 hour): " +
  26.                        timeBlock.GetNextPeriod(new TimeSpan(1, 0, 0)));
  27.     // > TimeBlock.GetNextPeriod(+1 hour): 22.02.2011 19:00:00 - 21:00:00 | 02:00:00
  28.     Console.WriteLine("TimeBlock.GetNextPeriod(-1 hour): " +
  29.                        timeBlock.GetNextPeriod(new TimeSpan(-1, 0, 0)));
  30.     // > TimeBlock.GetNextPeriod(-1 hour): 22.02.2011 17:00:00 - 19:00:00 | 02:00:00
  31. } // TimeBlockSample


Интервал времени

ITimeInterval определяет период времени как и ITimeRange - с помощью Start и End. Кроме того, добавлена возможность контроля интерпретации свойств Start и End с помощью перечисления IntervalEdge:
  • Closed: граничный момент времени включается в расчёты. Соответствует поведению ITimeRange
  • Open: граничный момент исключается из расчётов
Возможные варианты выглядят так:



Обычно границы интервала имеют установленное свойство IntervalEdge.Closed, что приводит к возникновению точки пересечения с прилегающими временнЫми периодами. Но если для точки пересечения установть свойство в IntervalEdge.Open, то пересечения не будет:

Код
  1. // --------
  2. public void TimeIntervalSample()
  3. {
  4.     // --- time interval 1 ---
  5.     TimeInterval timeInterval1 = new TimeInterval(
  6.       new DateTime(2011, 5, 8),
  7.       new DateTime(2011, 5, 9));
  8.     Console.WriteLine("TimeInterval1: " + timeInterval1);
  9.     // > TimeInterval1: [08.05.2011 - 09.05.2011] | 1.00:00
  10.  
  11.     // --- time interval 2 ---
  12.     TimeInterval timeInterval2 = new TimeInterval(
  13.       timeInterval1.End,
  14.       timeInterval1.End.AddDays(1));
  15.     Console.WriteLine("TimeInterval2: " + timeInterval2);
  16.     // > TimeInterval2: [09.05.2011 - 10.05.2011] | 1.00:00
  17.  
  18.     // --- relation ---
  19.     Console.WriteLine("Relation: " + timeInterval1.GetRelation(timeInterval2));
  20.     // > Relation: EndTouching
  21.     Console.WriteLine("Intersection: " +
  22.                        timeInterval1.GetIntersection(timeInterval2));
  23.     // > Intersection: [09.05.2011]
  24.  
  25.     timeInterval1.EndEdge = IntervalEdge.Open;
  26.     Console.WriteLine("TimeInterval1: " + timeInterval1);
  27.     // > TimeInterval1: [08.05.2011 - 09.05.2011) | 1.00:00
  28.  
  29.     timeInterval2.StartEdge = IntervalEdge.Open;
  30.     Console.WriteLine("TimeInterval2: " + timeInterval2);
  31.     // > TimeInterval2: (09.05.2011 - 10.05.2011] | 1.00:00
  32.  
  33.     // --- relation ---
  34.     Console.WriteLine("Relation: " + timeInterval1.GetRelation(timeInterval2));
  35.     // > Relation: Before
  36.     Console.WriteLine("Intersection: " +
  37.                        timeInterval1.GetIntersection(timeInterval2));
  38.     // > Intersection:
  39. } // TimeIntervalSample

В некоторых случаях, например, в случае необходимости поиска разрывов во временнЫх периодах, исключение границ периодов может привести к нежелательным результатам. В таких ситуациях возможно отключение подобных исключений путём установки свойства IsIntervalEnabled.

Интервалы времени без границ могут быть созданы путём использования значений TimeSpec.MinPeriodDate для свойства Start и TimeSpec.MaxPeriodDate для свойства End.

Контейнер временнОго периода

В ежедневном применении расчёты со временем часто включают несколько периодов, которые можно собрать в контейнер и оперировать с ним как с единым целым. Библиотека Time Period предлагает такие виды контейнеров для периодов времени:



Все контейнеры реализуют ITimePeriod, поэтому все они представляют собой временнОй период. Также они могут быть использованы в расчётах других периодов, например, ITimeRange.

Интерфейс ITimePeriodContainer служит базой для всех контейнеров и поддерживает функциональность списка, наследуя от IList<ITimePeriod>.

Коллекция временнЫх периодов

ITimePeriodCollection может содержать произвольное количество элементов типа ITimePeriod и интерпретировать самое раннее свойство Start из всех этих элементов в качестве начала коллекции временных периодов. Соответственно, самый поздний конец одного из всех элементов служит концом коллекции периодов:



Вот такие операции можно совершить над коллекцией промежутков времени:



В следующем примере показано использование класса TimePeriodCollection, реализующего интерфейс ITimePeriodCollection:

Код
  1. // --------
  2. public void TimePeriodCollectionSample()
  3. {
  4.     TimePeriodCollection timePeriods = new TimePeriodCollection();
  5.  
  6.     DateTime testDay = new DateTime(2010, 7, 23);
  7.  
  8.     // --- items ---
  9.     timePeriods.Add(new TimeRange(TimeTrim.Hour(testDay, 8),
  10.                      TimeTrim.Hour(testDay, 11)));
  11.     timePeriods.Add(new TimeBlock(TimeTrim.Hour(testDay, 10), Duration.Hours(3)));
  12.     timePeriods.Add(new TimeRange(TimeTrim.Hour(testDay, 16, 15),
  13.                      TimeTrim.Hour(testDay, 18, 45)));
  14.     timePeriods.Add(new TimeRange(TimeTrim.Hour(testDay, 14),
  15.                      TimeTrim.Hour(testDay, 15, 30)));
  16.     Console.WriteLine("TimePeriodCollection: " + timePeriods);
  17.     // > TimePeriodCollection: Count = 4; 23.07.2010 08:00:00 - 18:45:00 | 0.10:45
  18.     Console.WriteLine("TimePeriodCollection.Items");
  19.     foreach (ITimePeriod timePeriod in timePeriods)
  20.     {
  21.         Console.WriteLine("Item: " + timePeriod);
  22.     }
  23.     // > Item: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
  24.     // > Item: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
  25.     // > Item: 23.07.2010 16:15:00 - 18:45:00 | 02:30:00
  26.     // > Item: 23.07.2010 14:00:00 - 15:30:00 | 01:30:00
  27.  
  28.     // --- intersection by moment ---
  29.     DateTime intersectionMoment = new DateTime(2010, 7, 23, 10, 30, 0);
  30.     ITimePeriodCollection momentIntersections =
  31.        timePeriods.IntersectionPeriods(intersectionMoment);
  32.     Console.WriteLine("TimePeriodCollection.IntesectionPeriods of " +
  33.                        intersectionMoment);
  34.     // > TimePeriodCollection.IntesectionPeriods of 23.07.2010 10:30:00
  35.     foreach (ITimePeriod momentIntersection in momentIntersections)
  36.     {
  37.         Console.WriteLine("Intersection: " + momentIntersection);
  38.     }
  39.     // > Intersection: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
  40.     // > Intersection: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
  41.  
  42.     // --- intersection by period ---
  43.     TimeRange intersectionPeriod =
  44.       new TimeRange(TimeTrim.Hour(testDay, 9),
  45.                      TimeTrim.Hour(testDay, 14, 30));
  46.     ITimePeriodCollection periodIntersections =
  47.       timePeriods.IntersectionPeriods(intersectionPeriod);
  48.     Console.WriteLine("TimePeriodCollection.IntesectionPeriods of " +
  49.                        intersectionPeriod);
  50.     // > TimePeriodCollection.IntesectionPeriods
  51.     //      of 23.07.2010 09:00:00 - 14:30:00 | 0.05:30
  52.     foreach (ITimePeriod periodIntersection in periodIntersections)
  53.     {
  54.         Console.WriteLine("Intersection: " + periodIntersection);
  55.     }
  56.     // > Intersection: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
  57.     // > Intersection: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
  58.     // > Intersection: 23.07.2010 14:00:00 - 15:30:00 | 01:30:00
  59. } // TimePeriodCollectionSample


Цепочки временных периодов

ITimePeriodChain соединяет несколько временных периодов типа ITimePeriod в цепочку и удостоверяется в том, что между ними нет разрывов:



Поскольку ITimePeriodChain должен уметь изменять позицию элементов, добавлять периоды, отмеченные "только для чтения" в него нельзя. Попытка такого добавления приведёт к генерации исключения NotSupportedException. ITimePeriodChain предлагает следующую функциональность:



В следующем примере показано использование класса TimePeriodChain, реализующего интерфейс ITimePeriodChain:

Код
  1. // --------
  2. public void TimePeriodChainSample()
  3. {
  4.     TimePeriodChain timePeriods = new TimePeriodChain();
  5.  
  6.     DateTime now = ClockProxy.Clock.Now;
  7.     DateTime testDay = new DateTime(2010, 7, 23);
  8.  
  9.     // --- add ---
  10.     timePeriods.Add(new TimeBlock(
  11.                      TimeTrim.Hour(testDay, 8), Duration.Hours(2)));
  12.     timePeriods.Add(new TimeBlock(now, Duration.Hours(1, 30)));
  13.     timePeriods.Add(new TimeBlock(now, Duration.Hour));
  14.     Console.WriteLine("TimePeriodChain.Add(): " + timePeriods);
  15.     // > TimePeriodChain.Add(): Count = 3; 23.07.2010 08:00:00 - 12:30:00 | 0.04:30
  16.     foreach (ITimePeriod timePeriod in timePeriods)
  17.     {
  18.         Console.WriteLine("Item: " + timePeriod);
  19.     }
  20.     // > Item: 23.07.2010 08:00:00 - 10:00:00 | 02:00:00
  21.     // > Item: 23.07.2010 10:00:00 - 11:30:00 | 01:30:00
  22.     // > Item: 23.07.2010 11:30:00 - 12:30:00 | 01:00:00
  23.  
  24.     // --- insert ---
  25.     timePeriods.Insert(2, new TimeBlock(now, Duration.Minutes(45)));
  26.     Console.WriteLine("TimePeriodChain.Insert(): " + timePeriods);
  27.     // > TimePeriodChain.Insert(): Count = 4; 23.07.2010 08:00:00 - 13:15:00 | 0.05:15
  28.     foreach (ITimePeriod timePeriod in timePeriods)
  29.     {
  30.         Console.WriteLine("Item: " + timePeriod);
  31.     }
  32.     // > Item: 23.07.2010 08:00:00 - 10:00:00 | 02:00:00
  33.     // > Item: 23.07.2010 10:00:00 - 11:30:00 | 01:30:00
  34.     // > Item: 23.07.2010 11:30:00 - 12:15:00 | 00:45:00
  35.     // > Item: 23.07.2010 12:15:00 - 13:15:00 | 01:00:00
  36. } // TimePeriodChainSample

ВременнЫе периоды календаря

Расчёты календарных периодов должны учитывать такие случаи, как несовпадение конца предыдущего периода с началом следующего. Далее в примере показаны значения часов дня между 13.00 и 15.00:

13:00:00.0000000 - 13:59:59.9999999
14:00:00.0000000 - 14:59:59.9999999


Конец периода лежит немого раньше следующего старта, разница между ними составляет минимум один тик = 100 наносекундам. Это важный аспект, который не может быть проигнорирован в расчётах с участием временнЫх периодов.

Библиотека Time Period содержит интерфейс ITimePeriodMapper, который может конвертировать моменты периода времени в обоих направлениях. Согласно выше приведенному сценарию, это может быть достигнуто таким образом:

Код
  1. // --------
  2. public void TimePeriodMapperSample()
  3. {
  4.     TimeCalendar timeCalendar = new TimeCalendar();
  5.     CultureInfo ci = CultureInfo.InvariantCulture;
  6.  
  7.     DateTime start = new DateTime(2011, 3, 1, 13, 0, 0);
  8.     DateTime end = new DateTime(2011, 3, 1, 14, 0, 0);
  9.  
  10.     Console.WriteLine("Original start: {0}",
  11.                        start.ToString("HH:mm:ss.fffffff", ci));
  12.     // > Original start: 13:00:00.0000000
  13.     Console.WriteLine("Original end: {0}",
  14.                        end.ToString("HH:mm:ss.fffffff", ci));
  15.     // > Original end: 14:00:00.0000000
  16.  
  17.     Console.WriteLine("Mapping offset start: {0}", timeCalendar.StartOffset);
  18.     // > Mapping offset start: 00:00:00
  19.     Console.WriteLine("Mapping offset end: {0}", timeCalendar.EndOffset);
  20.     // > Mapping offset end: -00:00:00.0000001
  21.  
  22.     Console.WriteLine("Mapped start: {0}",
  23.       timeCalendar.MapStart(start).ToString("HH:mm:ss.fffffff", ci));
  24.     // > Mapped start: 13:00:00.0000000
  25.     Console.WriteLine("Mapped end: {0}",
  26.       timeCalendar.MapEnd(end).ToString("HH:mm:ss.fffffff", ci));
  27.     // > Mapped end: 13:59:59.9999999
  28. } // TimePeriodMapperSample


Календарь времени

Задача по интерпретации временнЫх периодов реализована в интерфейсе ITimeCalendar:



В интерфейсе ITimeCalendar реализована следующая функциональность:

  • Присвоение CultureInfo (по умолчанию присвоено CultureInfo текущего потока)
  • Установка соответствий (маппинг) границ периодов (ITimePeriodMapper)
  • Установка базового месяца года (по умолчанию = January)
  • Определение того, как интерпретировать календарные недели
  • Именование периодов, как, например, имя года (фискальный год, школьный год...)
  • Различные расчёты с использованием календаря

Наследуя от ITimePeriodMapper, установка соответствия границ временного периода выполняется свойствами StartOffset (по умолчанию = 0) и EndOffset (по умолчанию = -1 тик).

В следующем примере приведена настройка календаря для фискального года:

Код
  1. //--------
  2. public class FiscalTimeCalendar : TimeCalendar
  3. {
  4.  
  5.     // --------
  6.     public FiscalTimeCalendar()
  7.         : base(
  8.           new TimeCalendarConfig
  9.           {
  10.               YearBaseMonth = YearMonth.October,  //  Октябрь как базовый месяц года
  11.               YearWeekType = YearWeekType.Iso8601, // нумеруем недели по стандарту ISO 8601
  12.               YearType = YearType.FiscalYear// считаем годы фискальными годами
  13.           })
  14.     {
  15.     } // FiscalTimeCalendar
  16.  
  17. } // class FiscalTimeCalendar

Этот календарь можно использовать так:

Код
  1. // -----
  2. public void FiscalYearSample()
  3. {
  4.     FiscalTimeCalendar calendar = new FiscalTimeCalendar(); // используем фискальные периоды
  5.  
  6.     DateTime moment1 = new DateTime(2006, 9, 30);
  7.     Console.WriteLine("Fiscal Year of {0}: {1}", moment1.ToShortDateString(),
  8.                        new Year(moment1, calendar).YearName);
  9.     // > Fiscal Year of 30.09.2006: FY2005
  10.     Console.WriteLine("Fiscal Quarter of {0}: {1}", moment1.ToShortDateString(),
  11.                        new Quarter(moment1, calendar).QuarterOfYearName);
  12.     // > Fiscal Quarter of 30.09.2006: FQ4 2005
  13.  
  14.     DateTime moment2 = new DateTime(2006, 10, 1);
  15.     Console.WriteLine("Fiscal Year of {0}: {1}", moment2.ToShortDateString(),
  16.                        new Year(moment2, calendar).YearName);
  17.     // > Фискальный год 01.10.2006: FY2006
  18.     Console.WriteLine("Fiscal Quarter of {0}: {1}", moment1.ToShortDateString(),
  19.                        new Quarter(moment2, calendar).QuarterOfYearName);
  20.     // > Fiscal Quarter of 30.09.2006: FQ1 2006
  21. } // FiscalYearSample

Более подробное описание классов Year и Quarter дано ниже.

Элементы календаря

Для наиболее общих элементов календаря существуют специализированные классы:

Период времени Одинарный период Несколько периодов Ссылается на базовый месяц года
Год Year Years Да
Полгода Halfyear Halfyears Да
Квартал Quarter Quarters Да
Месяц Month Months -
Неделя года Week Weeks -
День Day Days -
Час Hour Hours -
Минута Minute Minutes -


Создавать элементы с несколькими периодами можно указав количество таких периодов.

На следующей диаграмме указаны элементы календаря для кварталов и месяцев, другие элементы аналогичны:



Все элементы календаря являются наследниками базового класса CalendarTimeRange, который в свою очередь наследует от TimeRange. CalendarTimeRange содержит календарь ITimeCalendar, что удостоверяет нас в неизменности временнОго периода после создания IsReadOnly=true).

Благодаря наследованию от TimePeriod, элементы календаря реализуют интерфейс ITimePeriod, что позволяет им быть использованными в расчётах с другими временнЫми периодами.

В следующем примере показаны разные календарные элементы:

Код
  1. // -----
  2. public void CalendarYearTimePeriodsSample()
  3. {
  4.     DateTime moment = new DateTime(2011, 8, 15);
  5.     Console.WriteLine("Calendar Periods of {0}:", moment.ToShortDateString());
  6.     // > Calendar Periods of 15.08.2011:
  7.     Console.WriteLine("Year     : {0}", new Year(moment));
  8.     Console.WriteLine("Halfyear : {0}", new Halfyear(moment));
  9.     Console.WriteLine("Quarter  : {0}", new Quarter(moment));
  10.     Console.WriteLine("Month    : {0}", new Month(moment));
  11.     Console.WriteLine("Week     : {0}", new Week(moment));
  12.     Console.WriteLine("Day      : {0}", new Day(moment));
  13.     Console.WriteLine("Hour     : {0}", new Hour(moment));
  14.     // > Year     : 2011; 01.01.2011 - 31.12.2011 | 364.23:59
  15.     // > Halfyear : HY2 2011; 01.07.2011 - 31.12.2011 | 183.23:59
  16.     // > Quarter  : Q3 2011; 01.07.2011 - 30.09.2011 | 91.23:59
  17.     // > Month    : August 2011; 01.08.2011 - 31.08.2011 | 30.23:59
  18.     // > Week     : w/c 33 2011; 15.08.2011 - 21.08.2011 | 6.23:59
  19.     // > Day      : Montag; 15.08.2011 - 15.08.2011 | 0.23:59
  20.     // > Hour     : 15.08.2011; 00:00 - 00:59 | 0.00:59
  21. } // CalendarYearTimePeriodsSample

Некоторые специфические элементы календаря содержат методы, дающие доступ к временнЫм периодам своих подэлементов. В следующем примере показаны кварталы календарного года:

Код
  1. // -----
  2. public void YearQuartersSample()
  3. {
  4.     Year year = new Year(2012);
  5.     ITimePeriodCollection quarters = year.GetQuarters();
  6.     Console.WriteLine("Кварталы года: {0}", year);
  7.     // > Кварталы года: 2012; 01.01.2012 - 31.12.2012 | 365.23:59
  8.     foreach (Quarter quarter in quarters)
  9.     {
  10.         Console.WriteLine("Quarter: {0}", quarter);
  11.     }
  12.     // > Квартал: Q1 2012; 01.01.2012 - 31.03.2012 | 90.23:59
  13.     // > Квартал: Q2 2012; 01.04.2012 - 30.06.2012 | 90.23:59
  14.     // > Квартал: Q3 2012; 01.07.2012 - 30.09.2012 | 91.23:59
  15.     // > Квартал: Q4 2012; 01.10.2012 - 31.12.2012 | 91.23:59
  16. } // YearQuartersSample


Год и периоды года

Особенность календарных элементов состоит в поддержке календарных периодов, которые отклоняются от нормального календарного года:



Начало года может быть установлено путём задания значения свойства ITimeCalendar.YearBaseMonth и будет использоваться такими элементами календаря как год, полугодие и квартал. В качестве значения для старта года может служить любой месяц. Обычный календарный год является частным случаем, когда YearBaseMonth = YearMonth.January.

Следующие свойства управляют интерпретацией границ между годами:

  • MultipleCalendarYears равен true, если период охватывает несколько календарных лет
  • IsCalendarYear/Halfyear/Quarter равен true, если период ссылается на один календарный год

В следующем примере показаны элементы календаря для фискального года:

Код
  1. // -----
  2. public void FiscalYearTimePeriodsSample()
  3. {
  4.     DateTime moment = new DateTime(2011, 8, 15);
  5.     FiscalTimeCalendar fiscalCalendar = new FiscalTimeCalendar();
  6.     Console.WriteLine("Fiscal Year Periods of {0}:", moment.ToShortDateString());
  7.     // > Fiscal Year Periods of 15.08.2011:
  8.     Console.WriteLine("Year     : {0}", new Year(moment, fiscalCalendar));
  9.     Console.WriteLine("Halfyear : {0}", new Halfyear(moment, fiscalCalendar));
  10.     Console.WriteLine("Quarter  : {0}", new Quarter(moment, fiscalCalendar));
  11.     // > Year     : FY2010; 01.10.2010 - 30.09.2011 | 364.23:59
  12.     // > Halfyear : FHY2 2010; 01.04.2011 - 30.09.2011 | 182.23:59
  13.     // > Quarter  : FQ4 2010; 01.07.2011 - 30.09.2011 | 91.23:59
  14. } // FiscalYearTimePeriodsSample

Перенос начала года влияет на все содержимые элементы и их операции:

Код
  1. // -----
  2. public void YearStartSample()
  3. {
  4.     TimeCalendar calendar = new TimeCalendar(
  5.       new TimeCalendarConfig { YearBaseMonth = YearMonth.February });
  6.  
  7.     Years years = new Years(2012, 2, calendar); // 2012-2013
  8.     Console.WriteLine("Quarters of Years (February): {0}", years);
  9.     // > Quarters of Years (February): 2012 - 2014; 01.02.2012 - 31.01.2014 | 730.23:59
  10.  
  11.     foreach (Year year in years.GetYears())
  12.     {
  13.         foreach (Quarter quarter in year.GetQuarters())
  14.         {
  15.             Console.WriteLine("Quarter: {0}", quarter);
  16.         }
  17.     }
  18.     // > Quarter: Q1 2012; 01.02.2012 - 30.04.2012 | 89.23:59
  19.     // > Quarter: Q2 2012; 01.05.2012 - 31.07.2012 | 91.23:59
  20.     // > Quarter: Q3 2012; 01.08.2012 - 31.10.2012 | 91.23:59
  21.     // > Quarter: Q4 2012; 01.11.2012 - 31.01.2013 | 91.23:59
  22.     // > Quarter: Q1 2013; 01.02.2013 - 30.04.2013 | 88.23:59
  23.     // > Quarter: Q2 2013; 01.05.2013 - 31.07.2013 | 91.23:59
  24.     // > Quarter: Q3 2013; 01.08.2013 - 31.10.2013 | 91.23:59
  25.     // > Quarter: Q4 2013; 01.11.2013 - 31.01.2014 | 91.23:59
  26. } // YearStartSample

Далее показано использование полезных и популярных функций:

Код
  1. //-----
  2. public bool IntersectsYear(DateTime start, DateTime end, int year)
  3. {
  4.     return new Year(year).IntersectsWith(new TimeRange(start, end));
  5. } // IntersectsYear
  6.  
  7. //-----
  8. public void GetDaysOfPastQuarter(DateTime moment,
  9.        out DateTime firstDay, out DateTime lastDay)
  10. {
  11.     TimeCalendar calendar = new TimeCalendar(
  12.       new TimeCalendarConfig { YearBaseMonth = YearMonth.October });
  13.     Quarter quarter = new Quarter(moment, calendar);
  14.     Quarter pastQuarter = quarter.GetPreviousQuarter();
  15.  
  16.     firstDay = pastQuarter.FirstDayStart;
  17.     lastDay = pastQuarter.LastDayStart;
  18. } // GetDaysOfPastQuarter
  19.  
  20. // ----------------------------------------------------------------------
  21. public DateTime GetFirstDayOfWeek(DateTime moment)
  22. {
  23.     return new Week(moment).FirstDayStart;
  24. } // GetFirstDayOfWeek
  25.  
  26. // ----------------------------------------------------------------------
  27. public bool IsInCurrentWeek(DateTime test)
  28. {
  29.     return new Week().HasInside(test);
  30. } // IsInCurrentWeek

Недели

Общепринято нумеровать недели года с 1 до 52/53. В .NET Framework есть метод Calendar.GetWeekOfYear, который позволяет получить номер недели для текущего момента времени. К сожалению, метод пользуется определением, данным в ISO 8601, что может привести к неправильной интерпретации и другим ошибкам поведения.

Библиотека Time Period содержит перечисление YearWeekType, которое контролирует вычисление номера недели календаря согласно ISO 8601. YearWeekType поддерживается интерфейсом ITimeCalendar, что приводит к возможности использования различных способов расчёта:

Код
  1. // -----
  2. // see also http://blogs.msdn.com/b/shawnste/archive/2006/01/24/517178.aspx
  3. public void CalendarWeekSample()
  4. {
  5.     DateTime testDate = new DateTime(2007, 12, 31);
  6.  
  7.     // .NET calendar week
  8.     TimeCalendar calendar = new TimeCalendar();
  9.     Console.WriteLine("Calendar Week of {0}: {1}", testDate.ToShortDateString(),
  10.                        new Week(testDate, calendar).WeekOfYear);
  11.     // > Calendar Week of 31.12.2007: 53
  12.  
  13.     // ISO 8601 calendar week
  14.     TimeCalendar calendarIso8601 = new TimeCalendar(
  15.       new TimeCalendarConfig { YearWeekType = YearWeekType.Iso8601 });
  16.     Console.WriteLine("ISO 8601 Week of {0}: {1}", testDate.ToShortDateString(),
  17.                        new Week(testDate, calendarIso8601).WeekOfYear);
  18.     // > ISO 8601 Week of 31.12.2007: 1
  19. } // CalendarWeekSample

Инструменты расчёта временнЫх периодов

Разница между двумя точками времени

Структура TimeSpan в .NET Framework просто предлагает временнОй диапазон для дней, часов, минут, секунд и миллисекунд. С точки зрения пользователя, есть необходимость представления диапазона месяцев и лет:

  • Последний визит был 1 год, 4 месяца и 12 дней назад
  • Ваш возраст: 28 лет

В библиотеке Time Period есть класс DateDiff, который производит вычисления разницы во времени между двумя датами, а также предоставляет доступ к диапазону времени, составляющем разницу. Он корректно работает с календарными периодами различной месячной длины:

Код
  1. // -----
  2. public void DateDiffSample()
  3. {
  4.     DateTime date1 = new DateTime(2009, 11, 8, 7, 13, 59);
  5.     Console.WriteLine("Date1: {0}", date1);
  6.     // > Date1: 08.11.2009 07:13:59
  7.     DateTime date2 = new DateTime(2011, 3, 20, 19, 55, 28);
  8.     Console.WriteLine("Date2: {0}", date2);
  9.     // > Date2: 20.03.2011 19:55:28
  10.  
  11.     DateDiff dateDiff = new DateDiff(date1, date2);
  12.  
  13.     // differences
  14.     Console.WriteLine("DateDiff.Years: {0}", dateDiff.Years);
  15.     // > DateDiff.Years: 1
  16.     Console.WriteLine("DateDiff.Quarters: {0}", dateDiff.Quarters);
  17.     // > DateDiff.Quarters: 5
  18.     Console.WriteLine("DateDiff.Months: {0}", dateDiff.Months);
  19.     // > DateDiff.Months: 16
  20.     Console.WriteLine("DateDiff.Weeks: {0}", dateDiff.Weeks);
  21.     // > DateDiff.Weeks: 70
  22.     Console.WriteLine("DateDiff.Days: {0}", dateDiff.Days);
  23.     // > DateDiff.Days: 497
  24.     Console.WriteLine("DateDiff.Weekdays: {0}", dateDiff.Weekdays);
  25.     // > DateDiff.Weekdays: 71
  26.     Console.WriteLine("DateDiff.Hours: {0}", dateDiff.Hours);
  27.     // > DateDiff.Hours: 11940
  28.     Console.WriteLine("DateDiff.Minutes: {0}", dateDiff.Minutes);
  29.     // > DateDiff.Minutes: 716441
  30.     Console.WriteLine("DateDiff.Seconds: {0}", dateDiff.Seconds);
  31.     // > DateDiff.Seconds: 42986489
  32.  
  33.     // elapsed
  34.     Console.WriteLine("DateDiff.ElapsedYears: {0}", dateDiff.ElapsedYears);
  35.     // > DateDiff.ElapsedYears: 1
  36.     Console.WriteLine("DateDiff.ElapsedMonths: {0}", dateDiff.ElapsedMonths);
  37.     // > DateDiff.ElapsedMonths: 4
  38.     Console.WriteLine("DateDiff.ElapsedDays: {0}", dateDiff.ElapsedDays);
  39.     // > DateDiff.ElapsedDays: 12
  40.     Console.WriteLine("DateDiff.ElapsedHours: {0}", dateDiff.ElapsedHours);
  41.     // > DateDiff.ElapsedHours: 12
  42.     Console.WriteLine("DateDiff.ElapsedMinutes: {0}", dateDiff.ElapsedMinutes);
  43.     // > DateDiff.ElapsedMinutes: 41
  44.     Console.WriteLine("DateDiff.ElapsedSeconds: {0}", dateDiff.ElapsedSeconds);
  45.     // > DateDiff.ElapsedSeconds: 29
  46.  
  47.     // description
  48.     Console.WriteLine("DateDiff.GetDescription(1): {0}", dateDiff.GetDescription(1));
  49.     // > DateDiff.GetDescription(1): 1 Year
  50.     Console.WriteLine("DateDiff.GetDescription(2): {0}", dateDiff.GetDescription(2));
  51.     // > DateDiff.GetDescription(2): 1 Year 4 Months
  52.     Console.WriteLine("DateDiff.GetDescription(3): {0}", dateDiff.GetDescription(3));
  53.     // > DateDiff.GetDescription(3): 1 Year 4 Months 12 Days
  54.     Console.WriteLine("DateDiff.GetDescription(4): {0}", dateDiff.GetDescription(4));
  55.     // > DateDiff.GetDescription(4): 1 Year 4 Months 12 Days 12 Hours
  56.     Console.WriteLine("DateDiff.GetDescription(5): {0}", dateDiff.GetDescription(5));
  57.     // > DateDiff.GetDescription(5): 1 Year 4 Months 12 Days 12 Hours 41 Mins
  58.     Console.WriteLine("DateDiff.GetDescription(6): {0}", dateDiff.GetDescription(6));
  59.     // > DateDiff.GetDescription(6): 1 Year 4 Months 12 Days 12 Hours 41 Mins 29 Secs
  60. } // DateDiffSample

Метод DateDiff.GetDescription может форматировать время с различным уровнем деталировки.

Расчёт временнЫх разрывов

TimeGapCalculator рассчитывает разрывы между временнЫми периодами в коллекции:



Интерпретация моментов времени может быть предметом приложения ITimePeriodMapper.

В следующем примере показано каким образом можно найти максимально большой разрыв между существующими заказами, если опустить выходные дни:

Код
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using Itenso.TimePeriod;
  6.  
  7. namespace TimePeriodLibrary
  8. {
  9.     class Program
  10.     {
  11.         static void Main(string[] args)
  12.         {
  13.  
  14.         }
  15.  
  16.         //-----
  17.         public void TimeGapCalculatorSample()
  18.         {
  19.             // simulation of some reservations
  20.             TimePeriodCollection reservations = new TimePeriodCollection();
  21.             reservations.Add(new Days(2011, 3, 7, 2));
  22.             reservations.Add(new Days(2011, 3, 16, 2));
  23.  
  24.             // the overall search range
  25.             CalendarTimeRange searchLimits = new CalendarTimeRange(
  26.                 new DateTime(2011, 3, 4), new DateTime(2011, 3, 21));
  27.  
  28.             // search the largest free time block
  29.             ICalendarTimeRange largestFreeTimeBlock =
  30.                 FindLargestFreeTimeBlock(reservations, searchLimits);
  31.             Console.WriteLine("Largest free time: " + largestFreeTimeBlock);
  32.             // > Largest free time: 09.03.2011 00:00:00 - 11.03.2011 23:59:59 | 2.23:59
  33.         } // TimeGapCalculatorSample
  34.  
  35.         //-----
  36.         public ICalendarTimeRange FindLargestFreeTimeBlock(
  37.                IEnumerable<ITimePeriod> reservations,
  38.                ITimePeriod searchLimits = null, bool excludeWeekends = true)
  39.         {
  40.             TimePeriodCollection bookedPeriods = new TimePeriodCollection(reservations);
  41.  
  42.             if (searchLimits == null)
  43.             {
  44.                 searchLimits = bookedPeriods; // use boundary of reservations
  45.             }
  46.  
  47.             if (excludeWeekends)
  48.             {
  49.                 Week currentWeek = new Week(searchLimits.Start);
  50.                 Week lastWeek = new Week(searchLimits.End);
  51.                 do
  52.                 {
  53.                     ITimePeriodCollection days = currentWeek.GetDays();
  54.                     foreach (Day day in days)
  55.                     {
  56.                         if (!searchLimits.HasInside(day))
  57.                         {
  58.                             continue; // outside of the search scope
  59.                         }
  60.                         if (day.DayOfWeek == DayOfWeek.Saturday ||
  61.                              day.DayOfWeek == DayOfWeek.Sunday)
  62.                         {
  63.                             bookedPeriods.Add(day); // // exclude weekend day
  64.                         }
  65.                     }
  66.                     currentWeek = currentWeek.GetNextWeek();
  67.                 } while (currentWeek.Start < lastWeek.Start);
  68.             }
  69.  
  70.             // calculate the gaps using the time calendar as period mapper
  71.             TimeGapCalculator<TimeRange> gapCalculator =
  72.               new TimeGapCalculator<TimeRange>(new TimeCalendar());
  73.             ITimePeriodCollection freeTimes =
  74.               gapCalculator.GetGaps(bookedPeriods, searchLimits);
  75.             if (freeTimes.Count == 0)
  76.             {
  77.                 return null;
  78.             }
  79.  
  80.             freeTimes.SortByDuration(); // move the largest gap to the start
  81.             return new CalendarTimeRange(freeTimes[0]);
  82.         } // FindLargestFreeTimeBlock
  83.  
  84.         //-----
  85.         public void DateDiffSample()
  86.         {
  87.             DateTime date1 = new DateTime(2009, 11, 8, 7, 13, 59);
  88.             Console.WriteLine("Date1: {0}", date1);
  89.             // > Date1: 08.11.2009 07:13:59
  90.             DateTime date2 = new DateTime(2011, 3, 20, 19, 55, 28);
  91.             Console.WriteLine("Date2: {0}", date2);
  92.             // > Date2: 20.03.2011 19:55:28
  93.  
  94.             DateDiff dateDiff = new DateDiff(date1, date2);
  95.  
  96.             // differences
  97.             Console.WriteLine("DateDiff.Years: {0}", dateDiff.Years);
  98.             // > DateDiff.Years: 1
  99.             Console.WriteLine("DateDiff.Quarters: {0}", dateDiff.Quarters);
  100.             // > DateDiff.Quarters: 5
  101.             Console.WriteLine("DateDiff.Months: {0}", dateDiff.Months);
  102.             // > DateDiff.Months: 16
  103.             Console.WriteLine("DateDiff.Weeks: {0}", dateDiff.Weeks);
  104.             // > DateDiff.Weeks: 70
  105.             Console.WriteLine("DateDiff.Days: {0}", dateDiff.Days);
  106.             // > DateDiff.Days: 497
  107.             Console.WriteLine("DateDiff.Weekdays: {0}", dateDiff.Weekdays);
  108.             // > DateDiff.Weekdays: 71
  109.             Console.WriteLine("DateDiff.Hours: {0}", dateDiff.Hours);
  110.             // > DateDiff.Hours: 11940
  111.             Console.WriteLine("DateDiff.Minutes: {0}", dateDiff.Minutes);
  112.             // > DateDiff.Minutes: 716441
  113.             Console.WriteLine("DateDiff.Seconds: {0}", dateDiff.Seconds);
  114.             // > DateDiff.Seconds: 42986489
  115.  
  116.             // elapsed
  117.             Console.WriteLine("DateDiff.ElapsedYears: {0}", dateDiff.ElapsedYears);
  118.             // > DateDiff.ElapsedYears: 1
  119.             Console.WriteLine("DateDiff.ElapsedMonths: {0}", dateDiff.ElapsedMonths);
  120.             // > DateDiff.ElapsedMonths: 4
  121.             Console.WriteLine("DateDiff.ElapsedDays: {0}", dateDiff.ElapsedDays);
  122.             // > DateDiff.ElapsedDays: 12
  123.             Console.WriteLine("DateDiff.ElapsedHours: {0}", dateDiff.ElapsedHours);
  124.             // > DateDiff.ElapsedHours: 12
  125.             Console.WriteLine("DateDiff.ElapsedMinutes: {0}", dateDiff.ElapsedMinutes);
  126.             // > DateDiff.ElapsedMinutes: 41
  127.             Console.WriteLine("DateDiff.ElapsedSeconds: {0}", dateDiff.ElapsedSeconds);
  128.             // > DateDiff.ElapsedSeconds: 29
  129.  
  130.             // description
  131.             Console.WriteLine("DateDiff.GetDescription(1): {0}", dateDiff.GetDescription(1));
  132.             // > DateDiff.GetDescription(1): 1 Year
  133.             Console.WriteLine("DateDiff.GetDescription(2): {0}", dateDiff.GetDescription(2));
  134.             // > DateDiff.GetDescription(2): 1 Year 4 Months
  135.             Console.WriteLine("DateDiff.GetDescription(3): {0}", dateDiff.GetDescription(3));
  136.             // > DateDiff.GetDescription(3): 1 Year 4 Months 12 Days
  137.             Console.WriteLine("DateDiff.GetDescription(4): {0}", dateDiff.GetDescription(4));
  138.             // > DateDiff.GetDescription(4): 1 Year 4 Months 12 Days 12 Hours
  139.             Console.WriteLine("DateDiff.GetDescription(5): {0}", dateDiff.GetDescription(5));
  140.             // > DateDiff.GetDescription(5): 1 Year 4 Months 12 Days 12 Hours 41 Mins
  141.             Console.WriteLine("DateDiff.GetDescription(6): {0}", dateDiff.GetDescription(6));
  142.             // > DateDiff.GetDescription(6): 1 Year 4 Months 12 Days 12 Hours 41 Mins 29 Secs
  143.         } // DateDiffSample
  144.  
  145.  
  146.  
  147.         //-----
  148.         // see also http://blogs.msdn.com/b/shawnste/archive/2006/01/24/517178.aspx
  149.         public void CalendarWeekSample()
  150.         {
  151.             DateTime testDate = new DateTime(2007, 12, 31);
  152.  
  153.             // .NET calendar week
  154.             TimeCalendar calendar = new TimeCalendar();
  155.             Console.WriteLine("Calendar Week of {0}: {1}", testDate.ToShortDateString(),
  156.                                new Week(testDate, calendar).WeekOfYear);
  157.             // > Calendar Week of 31.12.2007: 53
  158.  
  159.             // ISO 8601 calendar week
  160.             TimeCalendar calendarIso8601 = new TimeCalendar(
  161.               new TimeCalendarConfig { YearWeekType = YearWeekType.Iso8601 });
  162.             Console.WriteLine("ISO 8601 Week of {0}: {1}", testDate.ToShortDateString(),
  163.                                new Week(testDate, calendarIso8601).WeekOfYear);
  164.             // > ISO 8601 Week of 31.12.2007: 1
  165.         } // CalendarWeekSample
  166.  
  167.         //-----
  168.         public bool IntersectsYear(DateTime start, DateTime end, int year)
  169.         {
  170.             return new Year(year).IntersectsWith(new TimeRange(start, end));
  171.         } // IntersectsYear
  172.  
  173.         //-----
  174.         public void GetDaysOfPastQuarter(DateTime moment,
  175.                out DateTime firstDay, out DateTime lastDay)
  176.         {
  177.             TimeCalendar calendar = new TimeCalendar(
  178.               new TimeCalendarConfig { YearBaseMonth = YearMonth.October });
  179.             Quarter quarter = new Quarter(moment, calendar);
  180.             Quarter pastQuarter = quarter.GetPreviousQuarter();
  181.  
  182.             firstDay = pastQuarter.FirstDayStart;
  183.             lastDay = pastQuarter.LastDayStart;
  184.         } // GetDaysOfPastQuarter
  185.  
  186.         //-----
  187.         public DateTime GetFirstDayOfWeek(DateTime moment)
  188.         {
  189.             return new Week(moment).FirstDayStart;
  190.         } // GetFirstDayOfWeek
  191.  
  192.         //-----
  193.         public bool IsInCurrentWeek(DateTime test)
  194.         {
  195.             return new Week().HasInside(test);
  196.         } // IsInCurrentWeek
  197.  
  198.         //-----
  199.         public void YearStartSample()
  200.         {
  201.             TimeCalendar calendar = new TimeCalendar(
  202.               new TimeCalendarConfig { YearBaseMonth = YearMonth.February });
  203.  
  204.             Years years = new Years(2012, 2, calendar); // 2012-2013
  205.             Console.WriteLine("Quarters of Years (February): {0}", years);
  206.             // > Quarters of Years (February): 2012 - 2014; 01.02.2012 - 31.01.2014 | 730.23:59
  207.  
  208.             foreach (Year year in years.GetYears())
  209.             {
  210.                 foreach (Quarter quarter in year.GetQuarters())
  211.                 {
  212.                     Console.WriteLine("Quarter: {0}", quarter);
  213.                 }
  214.             }
  215.             // > Quarter: Q1 2012; 01.02.2012 - 30.04.2012 | 89.23:59
  216.             // > Quarter: Q2 2012; 01.05.2012 - 31.07.2012 | 91.23:59
  217.             // > Quarter: Q3 2012; 01.08.2012 - 31.10.2012 | 91.23:59
  218.             // > Quarter: Q4 2012; 01.11.2012 - 31.01.2013 | 91.23:59
  219.             // > Quarter: Q1 2013; 01.02.2013 - 30.04.2013 | 88.23:59
  220.             // > Quarter: Q2 2013; 01.05.2013 - 31.07.2013 | 91.23:59
  221.             // > Quarter: Q3 2013; 01.08.2013 - 31.10.2013 | 91.23:59
  222.             // > Quarter: Q4 2013; 01.11.2013 - 31.01.2014 | 91.23:59
  223.         } // YearStartSample
  224.  
  225.         // -----
  226.         public void FiscalYearTimePeriodsSample()
  227.         {
  228.             DateTime moment = new DateTime(2011, 8, 15);
  229.             FiscalTimeCalendar fiscalCalendar = new FiscalTimeCalendar();
  230.             Console.WriteLine("Fiscal Year Periods of {0}:", moment.ToShortDateString());
  231.             // > Fiscal Year Periods of 15.08.2011:
  232.             Console.WriteLine("Year     : {0}", new Year(moment, fiscalCalendar));
  233.             Console.WriteLine("Halfyear : {0}", new Halfyear(moment, fiscalCalendar));
  234.             Console.WriteLine("Quarter  : {0}", new Quarter(moment, fiscalCalendar));
  235.             // > Year     : FY2010; 01.10.2010 - 30.09.2011 | 364.23:59
  236.             // > Halfyear : FHY2 2010; 01.04.2011 - 30.09.2011 | 182.23:59
  237.             // > Quarter  : FQ4 2010; 01.07.2011 - 30.09.2011 | 91.23:59
  238.         } // FiscalYearTimePeriodsSample
  239.  
  240.  
  241.         // -----
  242.         public void YearQuartersSample()
  243.         {
  244.             Year year = new Year(2012);
  245.             ITimePeriodCollection quarters = year.GetQuarters();
  246.             Console.WriteLine("Quarters of Year: {0}", year);
  247.             // > Quarters of Year: 2012; 01.01.2012 - 31.12.2012 | 365.23:59
  248.             foreach (Quarter quarter in quarters)
  249.             {
  250.                 Console.WriteLine("Quarter: {0}", quarter);
  251.             }
  252.             // > Quarter: Q1 2012; 01.01.2012 - 31.03.2012 | 90.23:59
  253.             // > Quarter: Q2 2012; 01.04.2012 - 30.06.2012 | 90.23:59
  254.             // > Quarter: Q3 2012; 01.07.2012 - 30.09.2012 | 91.23:59
  255.             // > Quarter: Q4 2012; 01.10.2012 - 31.12.2012 | 91.23:59
  256.         } // YearQuartersSample
  257.  
  258.  
  259.  
  260.         // -----
  261.         public void CalendarYearTimePeriodsSample()
  262.         {
  263.             DateTime moment = new DateTime(2011, 8, 15);
  264.             Console.WriteLine("Calendar Periods of {0}:", moment.ToShortDateString());
  265.             // > Calendar Periods of 15.08.2011:
  266.             Console.WriteLine("Year     : {0}", new Year(moment));
  267.             Console.WriteLine("Halfyear : {0}", new Halfyear(moment));
  268.             Console.WriteLine("Quarter  : {0}", new Quarter(moment));
  269.             Console.WriteLine("Month    : {0}", new Month(moment));
  270.             Console.WriteLine("Week     : {0}", new Week(moment));
  271.             Console.WriteLine("Day      : {0}", new Day(moment));
  272.             Console.WriteLine("Hour     : {0}", new Hour(moment));
  273.             // > Year     : 2011; 01.01.2011 - 31.12.2011 | 364.23:59
  274.             // > Halfyear : HY2 2011; 01.07.2011 - 31.12.2011 | 183.23:59
  275.             // > Quarter  : Q3 2011; 01.07.2011 - 30.09.2011 | 91.23:59
  276.             // > Month    : August 2011; 01.08.2011 - 31.08.2011 | 30.23:59
  277.             // > Week     : w/c 33 2011; 15.08.2011 - 21.08.2011 | 6.23:59
  278.             // > Day      : Montag; 15.08.2011 - 15.08.2011 | 0.23:59
  279.             // > Hour     : 15.08.2011; 00:00 - 00:59 | 0.00:59
  280.         } // CalendarYearTimePeriodsSample
  281.  
  282.  
  283.  
  284.         //-----
  285.         public void FiscalYearSample()
  286.         {
  287.             FiscalTimeCalendar calendar = new FiscalTimeCalendar(); // use fiscal periods
  288.  
  289.             DateTime moment1 = new DateTime(2006, 9, 30);
  290.             Console.WriteLine("Fiscal Year of {0}: {1}", moment1.ToShortDateString(),
  291.                                new Year(moment1, calendar).YearName);
  292.             // > Fiscal Year of 30.09.2006: FY2005
  293.             Console.WriteLine("Fiscal Quarter of {0}: {1}", moment1.ToShortDateString(),
  294.                                new Quarter(moment1, calendar).QuarterOfYearName);
  295.             // > Fiscal Quarter of 30.09.2006: FQ4 2005
  296.  
  297.             DateTime moment2 = new DateTime(2006, 10, 1);
  298.             Console.WriteLine("Fiscal Year of {0}: {1}", moment2.ToShortDateString(),
  299.                                new Year(moment2, calendar).YearName);
  300.             // > Fiscal Year of 01.10.2006: FY2006
  301.             Console.WriteLine("Fiscal Quarter of {0}: {1}", moment1.ToShortDateString(),
  302.                                new Quarter(moment2, calendar).QuarterOfYearName);
  303.             // > Fiscal Quarter of 30.09.2006: FQ1 2006
  304.         } // FiscalYearSample
  305.  
  306.  
  307.  
  308. //-----
  309. public class FiscalTimeCalendar : TimeCalendar
  310. {
  311.  
  312.   //-----
  313.   public FiscalTimeCalendar()
  314.     : base(
  315.       new TimeCalendarConfig
  316.       {
  317.         YearBaseMonth = YearMonth.October,  //  October year base month
  318.         YearWeekType = YearWeekType.Iso8601, // ISO 8601 week numbering
  319.         YearType = YearType.FiscalYear// treat years as fiscal years
  320.       } )
  321.   {
  322.   } // FiscalTimeCalendar
  323.  
  324. } // class FiscalTimeCalendar
  325.  
  326.  
  327.  
  328.         //-----
  329.         public void TimePeriodChainSample()
  330.         {
  331.             TimePeriodChain timePeriods = new TimePeriodChain();
  332.  
  333.             DateTime now = ClockProxy.Clock.Now;
  334.             DateTime testDay = new DateTime(2010, 7, 23);
  335.  
  336.             // --- add ---
  337.             timePeriods.Add(new TimeBlock(
  338.                              TimeTrim.Hour(testDay, 8), Duration.Hours(2)));
  339.             timePeriods.Add(new TimeBlock(now, Duration.Hours(1, 30)));
  340.             timePeriods.Add(new TimeBlock(now, Duration.Hour));
  341.             Console.WriteLine("TimePeriodChain.Add(): " + timePeriods);
  342.             // > TimePeriodChain.Add(): Count = 3; 23.07.2010 08:00:00 - 12:30:00 | 0.04:30
  343.             foreach (ITimePeriod timePeriod in timePeriods)
  344.             {
  345.                 Console.WriteLine("Item: " + timePeriod);
  346.             }
  347.             // > Item: 23.07.2010 08:00:00 - 10:00:00 | 02:00:00
  348.             // > Item: 23.07.2010 10:00:00 - 11:30:00 | 01:30:00
  349.             // > Item: 23.07.2010 11:30:00 - 12:30:00 | 01:00:00
  350.  
  351.             // --- insert ---
  352.             timePeriods.Insert(2, new TimeBlock(now, Duration.Minutes(45)));
  353.             Console.WriteLine("TimePeriodChain.Insert(): " + timePeriods);
  354.             // > TimePeriodChain.Insert(): Count = 4; 23.07.2010 08:00:00 - 13:15:00 | 0.05:15
  355.             foreach (ITimePeriod timePeriod in timePeriods)
  356.             {
  357.                 Console.WriteLine("Item: " + timePeriod);
  358.             }
  359.             // > Item: 23.07.2010 08:00:00 - 10:00:00 | 02:00:00
  360.             // > Item: 23.07.2010 10:00:00 - 11:30:00 | 01:30:00
  361.             // > Item: 23.07.2010 11:30:00 - 12:15:00 | 00:45:00
  362.             // > Item: 23.07.2010 12:15:00 - 13:15:00 | 01:00:00
  363.         } // TimePeriodChainSample
  364.  
  365.  
  366.  
  367.         //-----
  368.         public void TimePeriodCollectionSample()
  369.         {
  370.             TimePeriodCollection timePeriods = new TimePeriodCollection();
  371.  
  372.             DateTime testDay = new DateTime(2010, 7, 23);
  373.  
  374.             // --- items ---
  375.             timePeriods.Add(new TimeRange(TimeTrim.Hour(testDay, 8),
  376.                              TimeTrim.Hour(testDay, 11)));
  377.             timePeriods.Add(new TimeBlock(TimeTrim.Hour(testDay, 10), Duration.Hours(3)));
  378.             timePeriods.Add(new TimeRange(TimeTrim.Hour(testDay, 16, 15),
  379.                              TimeTrim.Hour(testDay, 18, 45)));
  380.             timePeriods.Add(new TimeRange(TimeTrim.Hour(testDay, 14),
  381.                              TimeTrim.Hour(testDay, 15, 30)));
  382.             Console.WriteLine("TimePeriodCollection: " + timePeriods);
  383.             // > TimePeriodCollection: Count = 4; 23.07.2010 08:00:00 - 18:45:00 | 0.10:45
  384.             Console.WriteLine("TimePeriodCollection.Items");
  385.             foreach (ITimePeriod timePeriod in timePeriods)
  386.             {
  387.                 Console.WriteLine("Item: " + timePeriod);
  388.             }
  389.             // > Item: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
  390.             // > Item: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
  391.             // > Item: 23.07.2010 16:15:00 - 18:45:00 | 02:30:00
  392.             // > Item: 23.07.2010 14:00:00 - 15:30:00 | 01:30:00
  393.  
  394.             // --- intersection by moment ---
  395.             DateTime intersectionMoment = new DateTime(2010, 7, 23, 10, 30, 0);
  396.             ITimePeriodCollection momentIntersections =
  397.                timePeriods.IntersectionPeriods(intersectionMoment);
  398.             Console.WriteLine("TimePeriodCollection.IntesectionPeriods of " +
  399.                                intersectionMoment);
  400.             // > TimePeriodCollection.IntesectionPeriods of 23.07.2010 10:30:00
  401.             foreach (ITimePeriod momentIntersection in momentIntersections)
  402.             {
  403.                 Console.WriteLine("Intersection: " + momentIntersection);
  404.             }
  405.             // > Intersection: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
  406.             // > Intersection: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
  407.  
  408.             // --- intersection by period ---
  409.             TimeRange intersectionPeriod =
  410.               new TimeRange(TimeTrim.Hour(testDay, 9),
  411.                              TimeTrim.Hour(testDay, 14, 30));
  412.             ITimePeriodCollection periodIntersections =
  413.               timePeriods.IntersectionPeriods(intersectionPeriod);
  414.             Console.WriteLine("TimePeriodCollection.IntesectionPeriods of " +
  415.                                intersectionPeriod);
  416.             // > TimePeriodCollection.IntesectionPeriods
  417.             //      of 23.07.2010 09:00:00 - 14:30:00 | 0.05:30
  418.             foreach (ITimePeriod periodIntersection in periodIntersections)
  419.             {
  420.                 Console.WriteLine("Intersection: " + periodIntersection);
  421.             }
  422.             // > Intersection: 23.07.2010 08:00:00 - 11:00:00 | 03:00:00
  423.             // > Intersection: 23.07.2010 10:00:00 - 13:00:00 | 03:00:00
  424.             // > Intersection: 23.07.2010 14:00:00 - 15:30:00 | 01:30:00
  425.         } // TimePeriodCollectionSample
  426.  
  427.  
  428.  
  429.         //-----
  430.         public void TimeIntervalSample()
  431.         {
  432.             // --- time interval 1 ---
  433.             TimeInterval timeInterval1 = new TimeInterval(
  434.               new DateTime(2011, 5, 8),
  435.               new DateTime(2011, 5, 9));
  436.             Console.WriteLine("TimeInterval1: " + timeInterval1);
  437.             // > TimeInterval1: [08.05.2011 - 09.05.2011] | 1.00:00
  438.  
  439.             // --- time interval 2 ---
  440.             TimeInterval timeInterval2 = new TimeInterval(
  441.               timeInterval1.End,
  442.               timeInterval1.End.AddDays(1));
  443.             Console.WriteLine("TimeInterval2: " + timeInterval2);
  444.             // > TimeInterval2: [09.05.2011 - 10.05.2011] | 1.00:00
  445.  
  446.             // --- relation ---
  447.             Console.WriteLine("Relation: " + timeInterval1.GetRelation(timeInterval2));
  448.             // > Relation: EndTouching
  449.             Console.WriteLine("Intersection: " +
  450.                                timeInterval1.GetIntersection(timeInterval2));
  451.             // > Intersection: [09.05.2011]
  452.  
  453.             timeInterval1.EndEdge = IntervalEdge.Open;
  454.             Console.WriteLine("TimeInterval1: " + timeInterval1);
  455.             // > TimeInterval1: [08.05.2011 - 09.05.2011) | 1.00:00
  456.  
  457.             timeInterval2.StartEdge = IntervalEdge.Open;
  458.             Console.WriteLine("TimeInterval2: " + timeInterval2);
  459.             // > TimeInterval2: (09.05.2011 - 10.05.2011] | 1.00:00
  460.  
  461.             // --- relation ---
  462.             Console.WriteLine("Relation: " + timeInterval1.GetRelation(timeInterval2));
  463.             // > Relation: Before
  464.             Console.WriteLine("Intersection: " +
  465.                                timeInterval1.GetIntersection(timeInterval2));
  466.             // > Intersection:
  467.         } // TimeIntervalSample
  468.  
  469.         //-----
  470.         public void TimeBlockSample()
  471.         {
  472.             // --- time block ---
  473.             TimeBlock timeBlock = new TimeBlock(
  474.               new DateTime(2011, 2, 22, 11, 0, 0),
  475.               new TimeSpan(2, 0, 0));
  476.             Console.WriteLine("TimeBlock: " + timeBlock);
  477.             // > TimeBlock: 22.02.2011 11:00:00 - 13:00:00 | 02:00:00
  478.  
  479.             // --- modification ---
  480.             timeBlock.Start = new DateTime(2011, 2, 22, 15, 0, 0);
  481.             Console.WriteLine("TimeBlock.Start: " + timeBlock);
  482.             // > TimeBlock.Start: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
  483.             timeBlock.Move(new TimeSpan(1, 0, 0));
  484.             Console.WriteLine("TimeBlock.Move(1 hour): " + timeBlock);
  485.             // > TimeBlock.Move(1 hour): 22.02.2011 16:00:00 - 18:00:00 | 02:00:00
  486.  
  487.             // --- previous/next ---
  488.             Console.WriteLine("TimeBlock.GetPreviousPeriod(): " +
  489.                                timeBlock.GetPreviousPeriod());
  490.             // > TimeBlock.GetPreviousPeriod(): 22.02.2011 14:00:00 - 16:00:00 | 02:00:00
  491.             Console.WriteLine("TimeBlock.GetNextPeriod(): " + timeBlock.GetNextPeriod());
  492.             // > TimeBlock.GetNextPeriod(): 22.02.2011 18:00:00 - 20:00:00 | 02:00:00
  493.             Console.WriteLine("TimeBlock.GetNextPeriod(+1 hour): " +
  494.                                timeBlock.GetNextPeriod(new TimeSpan(1, 0, 0)));
  495.             // > TimeBlock.GetNextPeriod(+1 hour): 22.02.2011 19:00:00 - 21:00:00 | 02:00:00
  496.             Console.WriteLine("TimeBlock.GetNextPeriod(-1 hour): " +
  497.                                timeBlock.GetNextPeriod(new TimeSpan(-1, 0, 0)));
  498.             // > TimeBlock.GetNextPeriod(-1 hour): 22.02.2011 17:00:00 - 19:00:00 | 02:00:00
  499.         } // TimeBlockSample
  500.  
  501.         //-----
  502.         public bool IsValidReservation(DateTime start, DateTime end)
  503.         {
  504.             if (!TimeCompare.IsSameDay(start, end))
  505.             {
  506.                 return false;  // multiple day reservation
  507.             }
  508.  
  509.             TimeRange workingHours =
  510.               new TimeRange(TimeTrim.Hour(start, 8), TimeTrim.Hour(start, 18));
  511.             return workingHours.HasInside(new TimeRange(start, end));
  512.         } // IsValidReservation
  513.  
  514.         //-----
  515.         public void TimeRangeSample()
  516.         {
  517.             // --- time range 1 ---
  518.             TimeRange timeRange1 = new TimeRange(
  519.               new DateTime(2011, 2, 22, 14, 0, 0),
  520.               new DateTime(2011, 2, 22, 18, 0, 0));
  521.             Console.WriteLine("TimeRange1: " + timeRange1);
  522.             // > TimeRange1: 22.02.2011 14:00:00 - 18:00:00 | 04:00:00
  523.  
  524.             // --- time range 2 ---
  525.             TimeRange timeRange2 = new TimeRange(
  526.               new DateTime(2011, 2, 22, 15, 0, 0),
  527.               new TimeSpan(2, 0, 0));
  528.             Console.WriteLine("TimeRange2: " + timeRange2);
  529.             // > TimeRange2: 22.02.2011 15:00:00 - 17:00:00 | 02:00:00
  530.  
  531.             // --- time range 3 ---
  532.             TimeRange timeRange3 = new TimeRange(
  533.               new DateTime(2011, 2, 22, 16, 0, 0),
  534.               new DateTime(2011, 2, 22, 21, 0, 0));
  535.             Console.WriteLine("TimeRange3: " + timeRange3);
  536.             // > TimeRange3: 22.02.2011 16:00:00 - 21:00:00 | 05:00:00
  537.  
  538.             // --- relation ---
  539.             Console.WriteLine("TimeRange1.GetRelation( TimeRange2 ): " +
  540.                                timeRange1.GetRelation(timeRange2));
  541.             // > TimeRange1.GetRelation( TimeRange2 ): Enclosing
  542.             Console.WriteLine("TimeRange1.GetRelation( TimeRange3 ): " +
  543.                                timeRange1.GetRelation(timeRange3));
  544.             // > TimeRange1.GetRelation( TimeRange3 ): EndInside
  545.             Console.WriteLine("TimeRange3.GetRelation( TimeRange2 ): " +
  546.                                timeRange3.GetRelation(timeRange2));
  547.             // > TimeRange3.GetRelation( TimeRange2 ): StartInside
  548.  
  549.             // --- intersection ---
  550.             Console.WriteLine("TimeRange1.GetIntersection( TimeRange2 ): " +
  551.                                timeRange1.GetIntersection(timeRange2));
  552.             // > TimeRange1.GetIntersection( TimeRange2 ):
  553.             //             22.02.2011 15:00:00 - 17:00:00 | 02:00:00
  554.             Console.WriteLine("TimeRange1.GetIntersection( TimeRange3 ): " +
  555.                                timeRange1.GetIntersection(timeRange3));
  556.             // > TimeRange1.GetIntersection( TimeRange3 ):
  557.             //             22.02.2011 16:00:00 - 18:00:00 | 02:00:00
  558.             Console.WriteLine("TimeRange3.GetIntersection( TimeRange2 ): " +
  559.                                timeRange3.GetIntersection(timeRange2));
  560.             // > TimeRange3.GetIntersection( TimeRange2 ):
  561.             //             22.02.2011 16:00:00 - 17:00:00 | 01:00:00
  562.         } // TimeRangeSample
  563.     }
  564. }


Консолидация временнЫх периодов

Иногда есть необходимость в рассмотрении консолидированного вида перекрывающихся или граничащих временнЫх периодов. Например, в качестве антипода поиска временнЫх разрывов. Класс TimePeriodCombiner oподдерживает возможность консолидирования временнЫх периодов:



В следующем примере показано объединение временнЫх периодов согласно рисунка:

Код
  1. //-----
  2. public void TimePeriodCombinerSample()
  3. {
  4.     TimePeriodCollection periods = new TimePeriodCollection();
  5.  
  6.     periods.Add(new TimeRange(new DateTime(2011, 3, 01), new DateTime(2011, 3, 10)));
  7.     periods.Add(new TimeRange(new DateTime(2011, 3, 04), new DateTime(2011, 3, 08)));
  8.  
  9.     periods.Add(new TimeRange(new DateTime(2011, 3, 15), new DateTime(2011, 3, 18)));
  10.     periods.Add(new TimeRange(new DateTime(2011, 3, 18), new DateTime(2011, 3, 22)));
  11.     periods.Add(new TimeRange(new DateTime(2011, 3, 20), new DateTime(2011, 3, 24)));
  12.  
  13.     periods.Add(new TimeRange(new DateTime(2011, 3, 26), new DateTime(2011, 3, 30)));
  14.  
  15.     TimePeriodCombiner<TimeRange> periodCombiner = new TimePeriodCombiner<TimeRange>();
  16.     ITimePeriodCollection combinedPeriods = periodCombiner.CombinePeriods(periods);
  17.  
  18.     foreach (ITimePeriod combinedPeriod in combinedPeriods)
  19.     {
  20.         Console.WriteLine("Combined Period: " + combinedPeriod);
  21.     }
  22.     // > Combined Period: 01.03.2011 - 10.03.2011 | 9.00:00
  23.     // > Combined Period: 15.03.2011 - 24.03.2011 | 9.00:00
  24.     // > Combined Period: 26.03.2011 - 30.03.2011 | 4.00:00
  25. } // TimePeriodCombinerSample


Пересечения временнЫх периодов

Если временнЫе периоды должны быть проверены на пересечения (например, два заказа в один момент времени), то нам поможет класс TimePeriodIntersector:



По умолчанию, пересекающиеся периоды комбинируются в один. Для сохранения всех пересекающихся периодов параметр combinePeriods метода IntersectPeriods может быть установлен в false.

Далее показан пример применения TimePeriodIntersector:

Код
  1. //-----
  2. public void TimePeriodIntersectorSample()
  3. {
  4.     TimePeriodCollection periods = new TimePeriodCollection();
  5.  
  6.     periods.Add(new TimeRange(new DateTime(2011, 3, 01), new DateTime(2011, 3, 10)));
  7.     periods.Add(new TimeRange(new DateTime(2011, 3, 05), new DateTime(2011, 3, 15)));
  8.     periods.Add(new TimeRange(new DateTime(2011, 3, 12), new DateTime(2011, 3, 18)));
  9.  
  10.     periods.Add(new TimeRange(new DateTime(2011, 3, 20), new DateTime(2011, 3, 24)));
  11.     periods.Add(new TimeRange(new DateTime(2011, 3, 22), new DateTime(2011, 3, 28)));
  12.     periods.Add(new TimeRange(new DateTime(2011, 3, 24), new DateTime(2011, 3, 26)));
  13.  
  14.     TimePeriodIntersector<TimeRange> periodIntersector =
  15.                       new TimePeriodIntersector<TimeRange>();
  16.     ITimePeriodCollection intersectedPeriods = periodIntersector.IntersectPeriods(periods);
  17.  
  18.     foreach (ITimePeriod intersectedPeriod in intersectedPeriods)
  19.     {
  20.         Console.WriteLine("Intersected Period: " + intersectedPeriod);
  21.     }
  22.     // > Intersected Period: 05.03.2011 - 10.03.2011 | 5.00:00
  23.     // > Intersected Period: 12.03.2011 - 15.03.2011 | 3.00:00
  24.     // > Intersected Period: 22.03.2011 - 26.03.2011 | 4.00:00
  25. } // TimePeriodIntersectorSample


Вычитание временнЫх периодов

С помощью класса TimePeriodSubtractor вы можете вычитать временнЫе периоды (вычитаемое) из других временных периодов (уменьшаемое):



В результате получаем разницу между двумя коллекциями периодов:

Код
  1.     //-----
  2.     public void TimePeriodSubtractorSample()
  3.     {
  4.         
  5.         DateTime moment = new DateTime(2012, 1, 29);
  6.         TimePeriodCollection sourcePeriods = new TimePeriodCollection
  7. {
  8.     new TimeRange( moment.AddHours( 2 ), moment.AddDays( 1 ) )
  9. };
  10.  
  11.         TimePeriodCollection subtractingPeriods = new TimePeriodCollection
  12. {
  13.     new TimeRange( moment.AddHours( 6 ), moment.AddHours( 10 ) ),
  14.     new TimeRange( moment.AddHours( 12 ), moment.AddHours( 16 ) )
  15. };
  16.         TimePeriodSubtractor<TimeRange> subtractor = new TimePeriodSubtractor<TimeRange>();
  17.         ITimePeriodCollection subtractedPeriods =
  18.           subtractor.SubtractPeriods(sourcePeriods, subtractingPeriods);
  19.         foreach (TimeRange subtractedPeriod in subtractedPeriods)
  20.         {
  21.             Console.WriteLine("Subtracted Period: {0}", subtractedPeriod);
  22.         }
  23.         // > Subtracted Period : 29.01.2012 02:00:00 - 06:00:00 | 0.04:00
  24.         // > Subtracted Period : 29.01.2012 10:00:00 - 12:00:00 | 0.02:00
  25.         // > Subtracted Period : 29.01.2012 16:00:00 - 30.01.2012 00:00:00 | 0.08:00
  26.     } // TimePeriodSubtractorSample</timerange></timerange>


Сложение и вычитание дат

Часто проблема состоит в том, чтобы добавить некий временной период к конкретной дате и в результате получить некоторый момент времени. То, что поначалу звучит просто, при более пристальном рассмотрении усложняется несколькими факторами:
  • нужно учесть только часы работы
  • выходные, праздники, периоды поддержки и обслуживания должны быть исключены
Поскольку существует столько дополнительных требований, общепринятая арифметика с датами не годится. В таких случаях нам поможет класс DateAdd:



Хотя имя класса говорит о противоположном, есть возможность выполнения не только сложения, но и вычитания. Особенность класса DateAdd заключается в его способности указания периодов для включения в DateAdd.IncludePeriods, а также исключения некоторых периодов с помощью DateAdd.ExcludePeriods. Есть возможность указания как одного свойства, так и обоих вместе. Если оба свойства не определены, инструмент ведёт себя эквивалентно DateTime.Add и DateTime.Subtract.

Пример использования класса DateAdd:

Код
  1. //-----
  2. public void DateAddSample()
  3. {
  4.     DateAdd dateAdd = new DateAdd();
  5.  
  6.     dateAdd.IncludePeriods.Add(new TimeRange(new DateTime(2011, 3, 17),
  7.                                 new DateTime(2011, 4, 20)));
  8.  
  9.     // setup some periods to exclude
  10.     dateAdd.ExcludePeriods.Add(new TimeRange(
  11.       new DateTime(2011, 3, 22), new DateTime(2011, 3, 25)));
  12.     dateAdd.ExcludePeriods.Add(new TimeRange(
  13.       new DateTime(2011, 4, 1), new DateTime(2011, 4, 7)));
  14.     dateAdd.ExcludePeriods.Add(new TimeRange(
  15.       new DateTime(2011, 4, 15), new DateTime(2011, 4, 16)));
  16.  
  17.     // positive
  18.     DateTime dateDiffPositive = new DateTime(2011, 3, 19);
  19.     DateTime? positive1 = dateAdd.Add(dateDiffPositive, Duration.Hours(1));
  20.     Console.WriteLine("DateAdd Positive1: {0}", positive1);
  21.     // > DateAdd Positive1: 19.03.2011 01:00:00
  22.     DateTime? positive2 = dateAdd.Add(dateDiffPositive, Duration.Days(4));
  23.     Console.WriteLine("DateAdd Positive2: {0}", positive2);
  24.     // > DateAdd Positive2: 26.03.2011 00:00:00
  25.     DateTime? positive3 = dateAdd.Add(dateDiffPositive, Duration.Days(17));
  26.     Console.WriteLine("DateAdd Positive3: {0}", positive3);
  27.     // > DateAdd Positive3: 14.04.2011 00:00:00
  28.     DateTime? positive4 = dateAdd.Add(dateDiffPositive, Duration.Days(20));
  29.     Console.WriteLine("DateAdd Positive4: {0}", positive4);
  30.     // > DateAdd Positive4: 18.04.2011 00:00:00
  31.  
  32.     // negative
  33.     DateTime dateDiffNegative = new DateTime(2011, 4, 18);
  34.     DateTime? negative1 = dateAdd.Add(dateDiffNegative, Duration.Hours(-1));
  35.     Console.WriteLine("DateAdd Negative1: {0}", negative1);
  36.     // > DateAdd Negative1: 17.04.2011 23:00:00
  37.     DateTime? negative2 = dateAdd.Add(dateDiffNegative, Duration.Days(-4));
  38.     Console.WriteLine("DateAdd Negative2: {0}", negative2);
  39.     // > DateAdd Negative2: 13.04.2011 00:00:00
  40.     DateTime? negative3 = dateAdd.Add(dateDiffNegative, Duration.Days(-17));
  41.     Console.WriteLine("DateAdd Negative3: {0}", negative3);
  42.     // > DateAdd Negative3: 22.03.2011 00:00:00
  43.     DateTime? negative4 = dateAdd.Add(dateDiffNegative, Duration.Days(-20));
  44.     Console.WriteLine("DateAdd Negative4: {0}", negative4);
  45.     // > DateAdd Negative4: 19.03.2011 00:00:00
  46. } // DateAddSample

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

Код
  1. //-----
  2. public void CalendarDateAddSample()
  3. {
  4.     CalendarDateAdd calendarDateAdd = new CalendarDateAdd();
  5.     // weekdays
  6.     calendarDateAdd.AddWorkingWeekDays();
  7.     // holidays
  8.     calendarDateAdd.ExcludePeriods.Add(new Day(2011, 4, 5, calendarDateAdd.Calendar));
  9.     // working hours
  10.     calendarDateAdd.WorkingHours.Add(new HourRange(new Time(08, 30), new Time(12)));
  11.     calendarDateAdd.WorkingHours.Add(new HourRange(new Time(13, 30), new Time(18)));
  12.  
  13.     DateTime start = new DateTime(2011, 4, 1, 9, 0, 0);
  14.     TimeSpan offset = new TimeSpan(22, 0, 0); // 22 hours
  15.  
  16.     DateTime? end = calendarDateAdd.Add(start, offset);
  17.  
  18.     Console.WriteLine("start: {0}", start);
  19.     // > start: 01.04.2011 09:00:00
  20.     Console.WriteLine("offset: {0}", offset);
  21.     // > offset: 22:00:00
  22.     Console.WriteLine("end: {0}", end);
  23.     // > end: 06.04.2011 16:30:00
  24. } // CalendarDateAddSample


Поиск календарных периодов

CalendarPeriodCollector поддерживает возможность поиска некоторого календарного периода (периодов) по заданным условиям. Поиск может быть выполнен с использованием ICalendarPeriodCollectorFilter по таким видам периодов:

  • Поиск по годам
  • Поиск по месяцам
  • Поиск по дням месяца
  • Поиск по дням недели

Без установки фильтра все временные диапазоны периода будут считаться активными. Объединение может быть выполнено по следующим периодам:

  • Годы: CalendarPeriodCollector.CollectYears
  • Месяцы: CalendarPeriodCollector.CollectMonths
  • Дни: CalendarPeriodCollector.CollectDays
  • Часы: CalendarPeriodCollector.CollectHours

В нормальном режиме будут объединены все временнЫе диапазоны найденных периодов. Что, например, позволяет найти все часы дня используя CalendarPeriodCollector.CollectHours.

Для дальнейшего ограничения результата, временные диапазоны могут быть ограничены так:

  • Какой месяц года: ICalendarPeriodCollectorFilter.AddCollectingMonths
  • Какой день месяца: ICalendarPeriodCollectorFilter.AddCollectingDays
  • Какой час дня: ICalendarPeriodCollectorFilter.AddCollectingHours

Например, определив промежуток времени с 08:00 до 10:00, результат будет содержать только один временной период, включающий оба часа (в отличие от временного промежутка для каждого часа). Такая оптимизация особенно ценна (если не необходима) в случае объединения больших диапазонов времени.

В следующем примере собраны все рабочие часы пятниц январей нескольких лет:

Код
  1. //-----
  2. public void CalendarPeriodCollectorSample()
  3. {
  4.     CalendarPeriodCollectorFilter filter = new CalendarPeriodCollectorFilter();
  5.     filter.Months.Add(YearMonth.January); // only Januaries
  6.     filter.WeekDays.Add(DayOfWeek.Friday); // only Fridays
  7.     filter.CollectingHours.Add(new HourRange(8, 18)); // working hours
  8.  
  9.     CalendarTimeRange testPeriod =
  10.       new CalendarTimeRange(new DateTime(2010, 1, 1), new DateTime(2011, 12, 31));
  11.     Console.WriteLine("Calendar period collector of period: " + testPeriod);
  12.     // > Calendar period collector of period:
  13.     //            01.01.2010 00:00:00 - 30.12.2011 23:59:59 | 728.23:59
  14.  
  15.     CalendarPeriodCollector collector =
  16.             new CalendarPeriodCollector(filter, testPeriod);
  17.     collector.CollectHours();
  18.     foreach (ITimePeriod period in collector.Periods)
  19.     {
  20.         Console.WriteLine("Period: " + period);
  21.     }
  22.     // > Period: 01.01.2010; 08:00 - 17:59 | 0.09:59
  23.     // > Period: 08.01.2010; 08:00 - 17:59 | 0.09:59
  24.     // > Period: 15.01.2010; 08:00 - 17:59 | 0.09:59
  25.     // > Period: 22.01.2010; 08:00 - 17:59 | 0.09:59
  26.     // > Period: 29.01.2010; 08:00 - 17:59 | 0.09:59
  27.     // > Period: 07.01.2011; 08:00 - 17:59 | 0.09:59
  28.     // > Period: 14.01.2011; 08:00 - 17:59 | 0.09:59
  29.     // > Period: 21.01.2011; 08:00 - 17:59 | 0.09:59
  30.     // > Period: 28.01.2011; 08:00 - 17:59 | 0.09:59
  31. } // CalendarPeriodCollectorSample


Поиск дней

Часто возникает необходимость определить следующий рабочий день, имея число рабочих дней. При подсчёте необходимо нужно исключить выходные, дни обслуживания и поддержки, праздники и т.п.

Для решения таких задач существует класс DaySeeker. Аналогично CalendarPeriodCollector, этот класс может контролироваться предустановленными фильтрами. Далее показан поиск рабочих дней с исключением всех выходных и праздников:



Реализация выглядит так:

Код
  1. //-----
  2. public void DaySeekerSample()
  3. {
  4.     Day start = new Day(new DateTime(2011, 2, 15));
  5.     Console.WriteLine("DaySeeker Start: " + start);
  6.     // > DaySeeker Start: Dienstag; 15.02.2011 | 0.23:59
  7.  
  8.     CalendarVisitorFilter filter = new CalendarVisitorFilter();
  9.     filter.AddWorkingWeekDays(); // only working days
  10.     filter.ExcludePeriods.Add(new Week(2011, 9));  // week #9
  11.     Console.WriteLine("DaySeeker Holidays: " + filter.ExcludePeriods[0]);
  12.     // > DaySeeker Holidays: w/c 9 2011; 28.02.2011 - 06.03.2011 | 6.23:59
  13.  
  14.     DaySeeker daySeeker = new DaySeeker(filter);
  15.     Day day1 = daySeeker.FindDay(start, 3); // same working week
  16.     Console.WriteLine("DaySeeker(3): " + day1);
  17.     // > DaySeeker(3): Freitag; 18.02.2011 | 0.23:59
  18.  
  19.     Day day2 = daySeeker.FindDay(start, 4); // Saturday -> next Monday
  20.     Console.WriteLine("DaySeeker(4): " + day2);
  21.     // > DaySeeker(4): Montag; 21.02.2011 | 0.23:59
  22.  
  23.     Day day3 = daySeeker.FindDay(start, 9); // holidays -> next Monday
  24.     Console.WriteLine("DaySeeker(9): " + day3);
  25.     // > DaySeeker(9): Montag; 07.03.2011 | 0.23:59
  26. } // DaySeekerSample


Элементы окружения

В нескольких классах сосредоточены определения, относящиеся ко времени и базовые расчёты с ним:

TimeSpec Константы для времени и периодов
YearHalfyear/
YearQuarter/
YearMonth/
YearWeekType
Перечисления для полугодий, кварталов, месяцев, недель
TimeTool Операции модификаций значений дат и времени, а также указанных временнЫх периодов
TimeCompare Функции сравнения временнЫх периодов
TimeFormatter Форматирование временнЫх периодов
TimeTrim Функции выравнивания временнЫх периодов
Now Расчёты текущего момента для разных промежутков времени; например, начальный момент времени текущего квартала
Duration Расчёты для указанных промежутков времени
Date Часть "дата" класса DateTime
Time Часть "время" класса DateTime
CalendarVisitor Абстрактный базовый класс для итерации по календарным периодам
DateTimeSet Отсортированный список уникальных моментов времени
TimeLine Инструмент расчёта для разделения или комбинирования временных периодов


Тесты

Библиотека Time Period доступна в трёх версиях:

  • для .NET 2.0 с модульными тестами (Unit Tests)
  • для .NET для Silverlight 4
  • для .NET для Windows Phone 7

БОльшая часть классов проверены тестами NUnit. исходный код одинаов для всех трёх вариантов (см. ниже: Composite Library Development), но модульные тесты доступны только для полного .NET Framework.

Создание стабильно работающих тестов для функциональности, работающей со временем, непростая задача, поскольку множество факторов влияют на состояние тестируемых объектов:

  • Разные культуры используют разные календари
  • Функциональность, базирующаяся на DateTime.Now может (и так часто бывает) привести к разным результатам при исполнении и тестировании в разное время
  • ВременнЫе расчёты - особенно использующие промежутки времени - приводят к массе особых случаев

С учётом этого неудивительно, что код модульных тестов в три раза больше кода самой библиотеки.

Приложения

С целью визуализации объектов календаря, библиотека содержит демонстрационный проект Time Period Demo для консоли, Silverlight и Windows Phone.

Для расчётов временнЫх периодов доступно Silverlight приложение Calendar Period Collector (http://www.cpc.itenso.com/). Инструмент, в основном, является конфигурационным интерфейсом к большинству важных параметров класса CalendarPeriodCollectorFilter и может рассчитывать промежутки времени с помощью CalendarPeriodCollector. Результаты могут быть скопированы в буфер обмена и вставлены в Microsoft Excel.

Совместная разработка библиотеки

Следующее соглашение о наименованиях используется в библиотеке Time Period для разделения файлов для разных платформ, если есть такая необходимость:
  • FileName.Desktop.Extension
  • FileName.Silverlight.Extension
  • FileName.WindowsPhone.Extension

Имя библиотеки DLL, также как и пространство имён, идентично для всех целевых платформ. Эти настройки можно изменить с помощью меню "Properties-Application-Assembly Name" и "Default namespace".

Результат для режимов Debug и Release размещается в разных каталогах для каждой платформы ("Properties-Build-Output Path"):

  • ..\Pub\Desktop.\
  • ..\Pub\Silverlight.\
  • ..\Pub\WindowsPhone\

Для избежания проблем с Visual Studio и некоторыми внешними инструментами, необходимо (!) размещать вывод временного компилятора в различные каталоги для каждой платформы. Это достигается с помощью выгрузки проекта (Unload Project) и вставки таких конфигурационных элементов для каждой целевой платформы:

Код
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Project ToolsVersion="4.0" DefaultTargets="Build"
  3.        xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  4.   ...
  5.   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
  6.     ...
  7.     <BaseIntermediateOutputPath>obj\\cf1 Desktop.Debug\</BaseIntermediateOutputPath>
  8.     <UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>
  9.     ...
  10.   </PropertyGroup>
  11.   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
  12.     ...
  13.     <BaseIntermediateOutputPath>obj\\cf1 Desktop.Release\</BaseIntermediateOutputPath>
  14.     <UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>
  15.     ...
  16.   </PropertyGroup>
  17.   ...
  18. </Project>

История версий

На момент перевода статьи доступна версия от 2го марта 2012 - v1.4.6.0.



Очень вольный перевод (с) В.Ф.Чужа ака hDrummer, 2012 г.

Комментарии

Shtin написал(а)…
Использовали эту библиотеку на боевом сайте, 29-ого февраля упала с ошибкой. Изучение исходников показала, что для вычисления некоторых интервалов она делает внути себя даты, используя день от одной даты, а месяц и год от другой. Дак вот не в каждом году есть 29-ое февраля.