Перейти к содержанию

Календари

Календари

Календари - это набор специального инструментария, который позволяет легко решать задачи по управлению и получению информации, связанной с рабочим и нерабочим временем. Календари оперируют датами исключительно в формате UTC. Так же стоит помнить, что каждый календарь рассчитывается не в какой-то конкретной временной зоне, а так, будто бы он подходит для любой установленной временной зоны. Календарь представляет из себя табличку, в которой записана заранее рассчитанная информация о том, является ли момент времени рабочим или нет. Даты в таблице календаря хранятся в UTC, а когда необходимо спроецировать дату на реальное смещение, то на них накладывается смещение врменной зоны, которая участвует в вычислении. Таким образом мы получаем, какое время по UTC будет в момент, когда в календаре был выбран необходимый момент времени, относительно участвовавшей в расчёте временной зоны.

Квант времени

Для отражения момента времени используется специальное понятие кванта времени. Существует два типа квантов времени - рабочий и не рабочий.

  • Рабочий квант - Длится 15 минут. На такие кванты разбит весь рабочий день.

  • Нерабочий квант - длится всё время с момента конца предыдущего рабочего кванта и до момента начала следующего рабочего кванта.

Таблица с квантами времени имеет следующую структуру:

  • QuantNumber - номер кванта.

  • StartTimeUTC – дата/время начала кванта (включительно).

  • EndTimeUTC – дата/время окончания кванта (не включительно).

  • Type - тип кванта (0 - рабочее время, 1 - выходной).

  • ID - идентификатор календаря (целочисленный идентификатор, который соответсвует одной из карточек календарей, заведённых в системе).

Рассчитанные кванты придерживаются нескольких основных правил:

  • Номера квантов идут по порядку и неразрывны.

  • Кванты нерабочего времени имеют те же номера, что и идущий перед ним квант.

  • Рабочее время разбито на кванты по 15 минут, нерабочее время заносится единым квантом.

  • Между квантами не должно быть промежутка по времени. Конец одного кванта равен началу следующего за ним кванта.

Для примера рассмотрим отрывок таблицы квантов календаря, когда в ней хранится один рассчитанный календарь:

QuantNumber
StartTimeUTC EndTimeUTC Type ID
0 2020-01-01 00:00:00 2020-01-01 09:00:00 1 0
1 2020-01-01 09:00:00 2020-01-01 09:15:00 0 0
2 2020-01-01 09:15:00 2020-01-01 09:30:00 0 0
3 2020-01-01 09:30:00 2020-01-01 09:45:00 0 0
14 2020-01-01 12:15:00 2020-01-01 12:30:00 0 0
15 2020-01-01 12:30:00 2020-01-01 12:45:00 0 0
16 2020-01-01 12:45:00 2020-01-01 13:00:00 0 0
16 2020-01-01 13:00:00 2020-01-01 14:00:00 1 0
17 2020-01-01 14:00:00 2020-01-01 14:15:00 0 0
18 2020-01-01 14:15:00 2020-01-01 14:30:00 0 0
19 2020-01-01 14:30:00 2020-01-01 14:45:00 0 0
22 2020-01-01 15:15:00 2020-01-01 15:30:00 0 0
30 2020-01-01 17:15:00 2020-01-01 17:30:00 0 0
31 2020-01-01 17:30:00 2020-01-01 17:45:00 0 0
32 2020-01-01 17:45:00 2020-01-01 18:00:00 0 0
32 2020-01-01 18:00:00 2020-01-06 09:00:00 1 0
33 2020-01-06 09:00:00 2020-01-06 09:15:00 0 0
34 2020-01-06 09:15:00 2020-01-06 09:30:00 0 0
35 2020-01-06 09:30:00 2020-01-06 09:45:00 0 0
48 2020-01-06 12:45:00 2020-01-06 13:00:00 0 0
48 2020-01-06 13:00:00 2020-01-06 14:00:00 1 0
49 2020-01-06 14:00:00 2020-01-06 14:15:00 0 0
50 2020-01-06 14:15:00 2020-01-06 14:30:00 0 0
64 2020-01-06 17:45:00 2020-01-06 18:00:00 0 0
64 2020-01-06 18:00:00 2020-01-07 09:00:00 1 0
65 2020-01-07 09:00:00 2020-01-07 09:15:00 0 0

Важно помнить, что календарь оперирует временем со смещением UTC. Видно, что квант с номером 0 - квант нерабочего времени, он заканчивается 2020-01-01 09:00. Следом за ним идут кванты рабочего времени по 15 минут, до момента 2020-01-01 13:00, когда начнётся обед (квант нерабочего времени), который длится до 2020-01-01 14:00. Далее до 2020-01-01 18:00 идут снова кванты рабочего времени по 15 минут, пока не появится квант нерабочего времени, отражающий ночь, и длящийся до 2020-01-06 09:00.

Настройки календарей

Для настройки каждого отдельного календаря в системе есть специальные карточки календарей, расположенные на рабочем месте “Администратор” в узле Календари → Календари.
Карточка календаря содержит в себе:

  • Начало и Окончание - в этих полях заполняется период, для которого рассчитывается календарь (см. Период действия календаря).
  • Числовой идентификатор - поле, связывающее карточку календаря и таблицу с квантами. Опираясь на значения этого поля можно понять, какой карточке с настройками календаря принадлежит конкретный квант из таблицы.
  • Тип календаря - ссылка на карточку “Тип календаря”. В данном случае это “Рабочая неделя” (см. Карточка типа календаря).
  • Название - название календаря.
  • Описание - описание календаря.
  • Кнопки Пересчитать и Проверить целостность (см. Расчет календаря и проверка ошибок).
  • Исключения (см. Исключения календаря).
  • Именованные диапазоны (см. Именованные диапазоны).

Карточка календаря

Подробнее о настройке календарей см. Руководство администратора.

Работа с календарями

Для работы с календарями существует специальное API - IBusinessCalendarService и набор хранимых функций и процедур. Для использования API достаточно отрезолвить его из Unity. Так как в большинстве случаев API календаря используется внутри расширений, достаточно запросить его в конструкторе расширения.

class SomeExtension : CardGetExtension { private readonly IBusinessCalendarService businessCalendarService;

public CalendarCardButtonsExtension( IBusinessCalendarService businessCalendarService) { this.businessCalendarService = businessCalendarService; } // ... }

Далее можно использовать любой из предоставленных в API методов. Так же есть специальный набор хранимых процедур, которые можно использовать внутри sql-запросов.

Проверить - является ли рабочим указанное в UTC дата/время

При помощи функции IsWorkTimeAsync можно проверить, является ли указанный момент времени рабочим.

/// <summary> /// Проверяет, является ли рабочим указанная дата и время в абстрактном времени календаря. /// Если указан параметр <paramref name="zoneOffset"/>, то и <paramref name="dateTime"/> должен быть задан в UTC. /// </summary> /// <param name="dateTime">Дата и время в абстрактном времени календаря.</param> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="zoneOffset">Смещение временной зоны.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Тип кванта времени.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<BusinessCalendarTimeType> IsWorkTimeAsync( DateTime dateTime, Guid calendarCardID, TimeSpan? zoneOffset = null, CancellationToken cancellationToken = default);

Асинхронная функция возвращает значение перечисления BusinessCalendarTimeType.

public enum BusinessCalendarTimeType { Work, // Рабочее время OffHour // Нерабочее время }

Так же можно воспользоваться хранимой функцией CalendarIsWorkTime.

-- MS SQL FUNCTION CalendarIsWorkTime(@StartDate datetime, @CalendarID int) RETURNS bit --PG SQL FUNCTION "CalendarIsWorkTime"(date_time timestamptz, calendar_id int) RETURNS bool

Функция принимает следующие параметры:

  • @StartDate/date_time - дата/время в абстрактном времени календаря со смещением UTC, для которой производится расчёт.

  • @CalendarID/calendar_id- целочисленный идентификатор календаря, для которого производится расчёт.

  • Возвращает - 0 - рабочее время, 1 - нерабочее время.

Описание логики работы

Допустим, необходимо проверить является ли выбранный момент времени рабочим временем по одному из календарей. Для примера будем использовать календарь по умолчанию с целочисленным идентификатором 0. Для этого можно вызвать метод API IsWorkTimeAsync или хранимую функцию CalendarIsWorkTime. Полученный результат сообщает о типе кванта, к которому относится запрашиваемый момент времени.

Для примера возьмём - 2020-01-01 12:49:00 (момент времени указывается в UTC).

Для того, чтобы понять является ли момент времени 2020-01-01 12:49:00 рабочим, нужно запросить из таблицы календаря квант, в который этот момент времени попадает. По типу полученного кванта можно будет понять, является ли момент времени рабочим или нет.

Если мы обратимся к таблице (см. выше), то мы увидим, что момент времени 2020-01-01 12:49:00 соответствует кванту с номером 16, который начинается 2020-01-01 12:45:00 и длится до 2020-01-01 13:00:00, а его тип 0. Значит квант является квантом рабочего времени и момент времени 2020-01-01 12:49:00 является рабочим временем.

Расчёт рабочего времени между датами.

При помощи функции GetDateDiffAsync - можно получить количество рабочих квантов между двумя моментами времени.

/// <summary> /// Рассчитывает рабочее время между датами. /// Если указан параметр <paramref name="zoneOffset"/>, то <paramref name="dateTimeStart"/> и <paramref name="dateTimeEnd"/> должны быть заданы в UTC. /// </summary> /// <param name="dateTimeStart">Первая дата в абстрактном времени календаря.</param> /// <param name="dateTimeEnd">Вторая дата в абстрактном времени календаря (должна быть больше первой).</param> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="zoneOffset">Смещение временной зоны.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Рабочее время между датами в квантах.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<long> GetDateDiffAsync( DateTime dateTimeStart, DateTime dateTimeEnd, Guid calendarCardID, TimeSpan? zoneOffset = null, CancellationToken cancellationToken = default);

Асинхронная функция возвращает количество рабочих квантов между двумя моментами времени, которые указаны в параметрах.

Так же можно воспользоваться хранимой функцией CalendarGetDateDiff.

--MS SQL FUNCTION CalendarGetDateDiff(@FirstDate datetime, @SecondDate datetime, @CalendarID int) RETURNS bigint --PG SQL FUNCTION "CalendarGetDateDiff"(first_date timestamptz, second_date timestamptz, calendar_id int) RETURNS bigint

Функция принимает следующие параметры:

  • @FirstDate/first_date - первая дата в UTC.

  • @SecondDate/second_date - вторая дата в UTC.

  • @CalendarID/calendar_id - целочисленный идентификатор календаря, для которого производится расчёт.

  • Возвращает количество рабочих квантов между датами (1 квант = 15 минут).

Описание логики работы

Допустим нам необходимо получить оставшееся на заданиях время в рабочих часах по одному из календарей. Для примера будем использовать календарь по умолчанию с целочисленным идентификатором 0. Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию. Не забываем, что полученный результат будет в рабочих квантах (интервалах по 15 минут).

Для примера возьмём моменты времени - 2020-01-01 09:29:00 и 2020-01-01 14:20:00 (моменты времени указываются в UTC). Для того, чтобы найти количество рабочих квантов между двумя моментами времени (2020-01-01 09:29:00 и 2020-01-01 14:20:00) необходимо найти два кванта, которые относятся к первому и второму моменту времени соответственно.

Если мы обратимся к таблице (см. выше), то увидим, что это кванты с номерами 2 и 18 соответственно. А затем вычесть из номера кванта второго момента времени номер кванта первого момента времени, получится 18-2=16. Полученная разница будет являться количеством рабочих квантов между двумя моментами времени. Так как 1 рабочий квант это 15 минут, то между моментами времени 2020-01-01 09:29:00 и 2020-01-01 14:20:00 будет 4 рабочих часа.

Отсчёт в квантах рабочего времени от указанной даты

При помощи функции AddWorkingQuantsToDateAsync - можно получить ближайшее рабочее время, которое наступит, если добавить к моменту времени некоторое количество рабочих квантов.

// <summary> /// Отсчёт рабочего времени от указанной даты. /// Если указан параметр <paramref name="zoneOffset"/>, то и <paramref name="dateTime"/> должен быть задан в UTC. /// Если указан параметр <paramref name="zoneOffset"/>, возвращаемое значение будет так же в UTC. /// Иначе - возвращаемое значение будет в абстрактном времени календаря. /// </summary> /// <param name="dateTime">Дата и время в абстрактном времени календаря, к которому производится добавление.</param> /// <param name="quants">Рабочее время в квантах.</param> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="zoneOffset">Смещение временной зоны.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Дата и время в абстрактном времени календаря. /// Либо в UTC, если был задан параметр <paramref name="zoneOffset"/>. /// Дата округляется до 15 минут в большую сторону.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<DateTime> AddWorkingQuantsToDateAsync( DateTime dateTime, long quants, Guid calendarCardID, TimeSpan? zoneOffset = null, CancellationToken cancellationToken = default);

Асинхронная функция возвращает ближайшее рабочее время, которое наступит, если добавить к моменту времени dateTime рабочие кванты в количестве quants.

Так же можно воспользоваться хранимой функцией CalendarAddWorkQuants.

--MS SQL FUNCTION CalendarAddWorkQuants(@StartDate datetime, @Quants int, @CalendarID int) RETURNS datetime --PG SQL FUNCTION "CalendarAddWorkQuants"(date_time timestamptz, quants_to_add bigint, calendar_id int) RETURNS timestampt

Функция принимает следующие параметры:

  • @StartDate/date_time - дата/время в абстрактном времени календаря со смещением UTC, к которому производится добавление.

  • @Quants/quants_to_add - рабочее время в квантах (1 квант = 15 минут).

  • @CalendarID/calendar_id - целочисленный идентификатор календаря, для которого производится расчёт.

  • Возвращает дата/время в абстрактном времени календаря со смещением UTC. Дата округляется до 15 минут в большую сторону.

Описание логики работы

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

Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию. Не забываем, что при обращении к календарю мы оперируем рабочими квантами (интервалами по 15 минут).

Для примера возьмём момент времени - 2020-01-01 09:25:00 (момент времени указывается в UTC) и 5 рабочих часов (20 рабочих квантов). Для того, чтобы найти момент времени в ближайшем рабочем дне, когда пройдут 5 рабочих часов относительно момента времени 2020-01-01 09:25:00 нужно для начала найти квант, к которому относится выбранный момент времени.

Если мы обратимся к таблице (см. выше), то увидим, что это квант с номером 2. Далее прибавим 20 квантов (5 часов) к 2-ум и получим 22. Найдём квант с номером 22 и типом 0. Начало этого кванта будет искомым моментом времени 2020-01-01 15:30:00.

Вычисление начала первого рабочего кванта рабочего дня

При помощи функции GetFirstQuantStartAsync - можно получить время начала первого рабочего кванта рабочего дня, полученного смещением относительно заданной даты.

/// <summary> /// Возвращает начало первого рабочего кванта рабочего дня, /// полученного смещением относительно заданной даты. /// Если указан параметр <paramref name="zoneOffset"/>, то и <paramref name="dateTime"/> должен быть задан в UTC. /// Если указан параметр <paramref name="zoneOffset"/>, возвращаемое значение будет так же в UTC. /// Иначе - возвращаемое значение будет в абстрактном времени календаря. /// </summary> /// <param name="dateTime">Дата в абстрактном времени календаря.</param> /// <param name="daysOffset">Смещение в рабочих днях.</param> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="zoneOffset">Смещение временной зоны.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Дата и время в абстрактном времени календаря начала первого рабочего кванта рабочего дня. /// Либо в UTC, если был задан параметр <paramref name="zoneOffset"/>.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<DateTime> GetFirstQuantStartAsync( DateTime dateTime, int daysOffset, Guid calendarCardID, TimeSpan? zoneOffset = null, CancellationToken cancellationToken = default);

Асинхронная функция возвращает начало первого рабочего кванта рабочего дня, полученного смещением на daysOffset относительно заданной даты.

Так же можно воспользоваться хранимой функцией CalendarGetFirstQuantStart.

--MS SQL FUNCTION CalendarGetFirstQuantStart(@StartDateTime datetime, @Offset int, @CalendarID int) RETURNS datetime --PG SQL FUNCTION "CalendarGetFirstQuantStart"(date_time timestamptz, days_to_add int, calendar_id int) RETURNS timestamptz

Функция принимает следующие параметры:

  • @StartDateTime/date_time - дата/время в абстрактном времени календаря со смещением UTC, относительно которой производится вычисление.

  • @Offset/days_to_add - смещение в рабочих днях.

  • @CalendarID/calendar_id - целочисленный идентификатор календаря, для которого производится расчёт.

  • Возвращает дата/время в абстрактном времени календаря со смещением UTC. Дата округляется до 15 минут в большую сторону.

Описание логики работы

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

Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию. Не забываем, что при обращении к календарю мы оперируем рабочими квантами (интервалами по 15 минут).

Для примера возьмём момент времени - 2020-01-01 09:25:00 (момент времени указывается в UTC) и 1 рабочий день (т.к. нас интересует начало следущего рабочего дня). Для того, чтобы найти момент времени, соответствующий началу рабочего дня, который наступит через 1 день относительно момента времени 2020-01-01 09:25:00, нужно для начала знать продолжительность дня в рабочих часах для календаря, по которому производится расчёт. Обратившись к настройкам из карточки типа календаря, мы получим количество часов в рабочем дне, заданное для данного календаря (см. Руководство администратора). В нашем случае это 8 часов в дне, т.к. для календаря по умолчанию используется тип календаря “Рабочая неделя”. Вычислив произведение из 8 часов дне и 4 квантов в часе, мы получим, что один день это 32 кванта. Далее нам надо найти начало рабочего времени в дне, соответсвующем моменту времени 2020-01-01 09:25:00. Для этого берётся первый рабочий квант (с типом 0), у которого время начала больше или равно 2020-01-01 00:00:00.

Если мы обратимся к таблице (см. выше), то увидим, что это квант с номером 1. Далее прибавим 32 кванта (1 день) к 1-му и получим 33. Найдём первый квант с номером больше 33 и типом 0. Начало этого кванта будет искомым моментом времени 2020-01-06 09:00:00.

Вычисление конца последнего рабочего кванта рабочего дня

При помощи функции GetLastQuantEndAsync - можно получить время конца последнего рабочего кванта рабочего дня, полученного смещением относительно заданной даты.

/// <summary> /// Возвращает конец последнего рабочего кванта рабочего дня, /// полученного смещением относительно заданной даты. /// Если указан параметр <paramref name="zoneOffset"/>, то и <paramref name="dateTime"/> должен быть задан в UTC. /// Если указан параметр <paramref name="zoneOffset"/>, возвращаемое значение будет так же в UTC. /// Иначе - возвращаемое значение будет в абстрактном времени календаря. /// </summary> /// <param name="dateTime">Дата в абстрактном времени календаря.</param> /// <param name="daysOffset">Смещение в рабочих днях.</param> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="zoneOffset">Смещение временной зоны.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Дата и время в абстрактном времени календаря конца последнего рабочего кванта рабочего дня. /// Либо в UTC, если был задан параметр <paramref name="zoneOffset"/>.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<DateTime> GetLastQuantEndAsync( DateTime dateTime, int daysOffset, Guid calendarCardID, TimeSpan? zoneOffset = null, CancellationToken cancellationToken = default);

Асинхронная функция возвращает конец последнего рабочего кванта рабочего дня, полученного смещением на daysOffset относительно заданной даты.

Так же можно воспользоваться хранимой функцией CalendarGetLastQuantEnd.

--MS SQL FUNCTION CalendarGetLastQuantEnd(@StartDateTime datetime, @Offset int, @CalendarID int) RETURNS datetime --PG SQl FUNCTION "CalendarGetLastQuantEnd"(date_time timestamptz, days_to_add int, calendar_id int) RETURNS timestamptz

Функция принимает следующие параметры:

  • @StartDateTime/date_time - дата/время в абстрактном времени календаря со смещением UTC, относительно которой производится вычисление.

  • @Offset/days_to_add - смещение в рабочих днях.

  • @CalendarID/calendar_id - целочисленный идентификатор календаря, для которого производится расчёт.

  • Возвращает дата/время в абстрактном времени календаря со смещением UTC. Дата округляется до 15 минут в большую сторону.

Описание логики работы

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

Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию. Не забываем, что при обращении к календарю мы оперируем рабочими квантами (интервалами по 15 минут).

Для примера возьмём момент времени - 2020-01-01 09:25:00 (момент времени указывается в UTC) и 1 рабочий день (т.к. нас интересует конец следущего рабочего дня). Для того, чтобы найти момент времени, соответствующий концу рабочего дня, который наступит через 1 день относительно момента времени 2020-01-01 09:25:00, нужно для начала найти начало этого рабочего дня при помощи функции CalendarGetFirstQuantStart. Обратившись к этой функции, мы получим, что начало следующего рабочего дня - 2020-01-06 09:00:00. Далее, чтобы ограничить выборку одним днём, вычислим дату начала следующего дня, это 2020-01-07 00:00:00. Теперь нам осталось найти последний квант с типом 0, у которого время окончания между 2020-01-06 09:00:00 и 2020-01-07 00:00:00.

Если мы обратимся к таблице (см. выше), то увидим, что это квант с номером 64. Конец этого кванта будет искомым моментом времени 2020-01-06 18:00:00.

Отсчёт в рабочих днях от указанной даты

При помощи функции AddWorkingDaysToDateAsync - можно получить ближайшее рабочее время, которое наступит, если добавить к моменту времени некоторое количество расчётных рабочих дней.

/// <summary> /// Возвращает дату рабочего времени путём смещением в рабочих днях относительно заданной даты. /// Если указан параметр <paramref name="zoneOffset"/>, то и <paramref name="dateTime"/> должен быть задан в UTC. /// Если указан параметр <paramref name="zoneOffset"/>, возвращаемое значение будет так же в UTC. /// Иначе - возвращаемое значение будет в абстрактном времени календаря. /// </summary> /// <param name="dateTime">Дата в абстрактном времени календаря или в UTC, если указан <paramref name="zoneOffset"/>.</param> /// <param name="daysOffset">Смещение в рабочих днях.</param> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="zoneOffset">Смещение временной зоны.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Дата рабочего времени в абстрактном времени календаря. Либо в UTC, если был задан <paramref name="zoneOffset"/>.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<DateTime> AddWorkingDaysToDateAsync( DateTime dateTime, double daysOffset, Guid calendarCardID, TimeSpan? zoneOffset = null, CancellationToken cancellationToken = default);

Асинхронная функция возвращает ближайшее рабочее время, которое наступит, если добавить к моменту времени dateTime рабочие дни в количестве daysOffset.

Так же можно воспользоваться хранимой функцией CalendarAddWorkingDaysToDate.

--MS SQL FUNCTION CalendarAddWorkingDaysToDate(@StartDateTime datetime, @Offset float, @CalendarID int) RETURNS datetime --PG SQL FUNCTION "CalendarAddWorkingDaysToDate"(date_time timestamptz, days_to_add float, calendar_id int) RETURNS timestamptz

Функция принимает следующие параметры:

  • @StartDateTime/date_time - дата/время в абстрактном времени календаря со смещением UTC, к которому производится добавление.

  • @Offset/days_to_add - рабочее время в днях.

  • @CalendarID/calendar_id - целочисленный идентификатор календаря, для которого производится расчёт.

  • Возвращает дата/время в абстрактном времени календаря со смещением UTC. Дата округляется до 15 минут в большую сторону.

Описание логики работы

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

Для этого мы делаем соответствующий запрос к API или вызываем хранимую функцию. Не забываем, что при обращении к календарю мы оперируем рабочими квантами (интервалами по 15 минут).

Для примера возьмём моменты времени - 2020-01-01 09:25:00 (момент времени указывается в абстрактном времени календаря со смещением в UTC) и 1,5 рабочих дня. Для того, чтобы найти момент времени в ближайшем рабочем дне, когда пройдут 1,5 рабочих дня относительно момента времени 2020-01-01 09:25:00, нужно для начала найти количество квантов в 1,5 рабочих днях. А для этого необходимо знать продолжительность дня в рабочих часах для календаря, по которому производится расчёт. Обратившись к настройкам из карточки типа календаря, мы получим количество часов в рабочем дне, заданное для данного календаря (см. Руководство администратора). В нашем случае это 8 часов в дне, т.к. для календаря по умолчанию используется тип календаря “Рабочая неделя”. Вычислив произведение из 8 часов дне и 4 квантов в часе, мы получим, что один день это 32 кванта. Далее вычислим количество квантов в искомом сроке: 32 * 1,5 = 48. Следовательно, должно пройти 48 рабочих квантов с момента времени 2020-01-01 09:25:00

Если мы обратимся к таблице (см. выше), то увидим, что моменту времени 2020-01-01 09:25:00 соответствует квант с номером 2. Далее прибавим 48 квантов (1,5 дня) к 2-ум и получим 50. Найдём квант с номером 50 и типом 0. Начало этого кванта будет искомым моментом времени 2020-01-06 14:15:00.

Отсчёт в фактических рабочих днях от указанной даты

При помощи функции CalendarAddWorkingDaysToDateExactAsync можно получить ближайшее рабочее время, которое наступит, если добавить к моменту времени некоторое количество фактических рабочих дней.

/// <summary> /// Добавляет нужное количество рабочих дней к дате. /// Если указан параметр <paramref name="zoneOffset"/>, то и <paramref name="dateTime"/> должен быть задан в UTC. /// Если указан параметр <paramref name="zoneOffset"/>, возвращаемое значение будет так же в UTC. /// Иначе - возвращаемое значение будет в абстрактном времени календаря. /// </summary> /// <param name="dateTime">Дата в абстрактном времени календаря или в UTC, если указан <paramref name="zoneOffset"/>.</param> /// <param name="interval">Количество рабочих дней.</param> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="zoneOffset">Смещение временной зоны.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Дата рабочего времени в абстрактном времени календаря. Либо в UTC, если был задан <paramref name="zoneOffset"/>.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<DateTime> CalendarAddWorkingDaysToDateExactAsync( DateTime dateTime, int interval, Guid calendarCardID, TimeSpan? zoneOffset = null, CancellationToken cancellationToken = default);

Асинхронная функция возвращает ближайшее рабочее время, которое наступит, если добавить к моменту времени dateTime рабочие дни в количестве interval.

Так же можно воспользоваться хранимой функцией CalendarAddWorkingDaysToDateExact.

--MS SQL FUNCTION CalendarAddWorkingDaysToDateExact(@StartDateTime datetime, @Interval int, @CalendarID int) RETURNS datetime --PG SQL FUNCTION "CalendarAddWorkingDaysToDateExact"(date_time timestamptz, days_to_add int, calendar_id int) RETURNS timestamptz

Функция принимает следующие параметры:

  • @StartDateTime/date_time - дата/время в абстрактном времени календаря со смещением UTC, к которому производится добавление.

  • @Interval/days_to_add - рабочее время в днях.

  • @CalendarID/calendar_id - целочисленный идентификатор календаря, для которого производится расчёт.

  • Возвращает дата/время в UTC. Дата округляется до 15 минут в большую сторону.

Описание логики работы

Берем подмножество квантов календаря за период, в котором гарантированно окажется нужно количество рабочих дней. Эмпирически это (нужное кол-во рабочих дней)*3+14. 14 - это самый длинные в мире каникулы (новогодние). Такая фильтрация нужна, чтобы не обрабатывать весь календарь, что ОЧЕНЬ медленно и ОЧЕНЬ тяжело для сервера. Выбираем все рабочие кванты за этот период. В итоге мы получаем, что на каждый рабочий день есть хотя бы один квант. Конвертируем дату\время начала кванта в дату и делаем дистинкт - т.е. у нас остается всего по одной строке на каждый рабочий день. В итоге мы получили список рабочих дней в этом периоде. Сортируем, нумеруем и выбираем нужный по номеру = (нужное кол-во рабочих дней).

Получение плановой даты добавлением рабочих дней

При помощи хранимой функции CalendarGetPlannedByWorkingDays можно вычислить дату, полученную из переданной в функцию даты добавлением к ней рабочего времени (в днях).

--MS SQL FUNCTION [CalendarGetPlannedByWorkingDays](@CalendarID int, @HoursInDay float, @StartDate datetime, @PlannedWorkingDays float) RETURNS datetime --PG SQL FUNCTION "CalendarGetPlannedByWorkingDays"(calendar_id int, hours_in_day float, date_time timestamptz, planned_working_days float) RETURNS timestamptz

Функция принимает следующие параметры:

  • @CalendarID/calendar_id - целочисленный идентификатор календаря, для которого производится расчёт.

  • @HoursInDay/hours_in_day - количество часов в рабочем дне календаря, для которого производится расчёт.

  • @StartDate/date_time - дата/время в абстрактном времени календаря со смещением UTC, к которому производится добавление.

  • @PlannedWorkingDays/planned_working_days - рабочее время в днях.

  • Возвращает дата/время в UTC. Дата округляется до 15 минут в большую сторону.

Описание логики работы Допустим нам нужно добавить 0,5 рабочих дня к моменту времени 2020-01-01 09:25:00 для календаря с 8-мью часами в рабочем дне. Находим, кванту с каким номером соответствует этот интервал времени. Вычисляем произведение HoursInDay, PlannedWorkingDays и умнажаем на 4 (потому что в часу 4 кванта) и полученный результат прибавляем к номеру найденного кванта.

Если мы обратимся к таблице (см. выше), то увидим, что моменту времени 2020-01-01 09:25:00 соответствует квант с номером 2. Далее прибавим 16 квантов (0,5 дней, при 8-ми часах в дне) к 2-ум и получим 18. Найдём квант с номером 18 и типом 0. Начало этого кванта будет искомым моментом времени 2020-01-01 14:15:00.

Получение номера дня в неделе

При помощи хранимой функции CalendarGetDayOfWeek можно вычислить дату, полученную из переданной в функцию даты добавлением к ней рабочего времени (в днях).

--MS SQL FUNCTION [CalendarGetDayOfWeek](@StartDateTime datetime) RETURNS INT --PG SQL FUNCTION "CalendarGetDayOfWeek"(date_time timestamptz) RETURNS int

Функция принимает следующие параметры:

  • @StartDateTime/date_time - дата/время в UTC.

  • Возвращает номер дня в неделе от 1 до 7.

Получение информации о временной зоне роли

При помощи функции GetRoleTimeZoneInfoAsync можно получить информацию о временной зоне для роли с указанным roleID.

/// <summary> /// Возвращает информацию о временной зоне для роли. /// </summary> /// <param name="roleID">Идентификатор роли.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Информация о временной зоне для роли.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<TimeZoneInfo> GetRoleTimeZoneInfoAsync( Guid roleID, CancellationToken cancellationToken = default);

Асинхронная функция возвращает объект типа TimeZoneInfo.

public class TimeZoneInfo { #region Constructors

public TimeZoneInfo(int timeZoneID, TimeSpan utcOffset, string? shortName, string? codeName) { this.TimeZoneID = timeZoneID; this.TimeZoneUtcOffset = utcOffset; this.TimeZoneShortName = shortName; this.TimeZoneCodeName = codeName; }

public TimeZoneInfo(int timeZoneID, int utcOffsetMinutes, string? shortName, string? codeName) { this.TimeZoneID = timeZoneID; this.TimeZoneUtcOffset = TimeSpan.FromMinutes(utcOffsetMinutes); this.TimeZoneShortName = shortName; this.TimeZoneCodeName = codeName; }

#endregion

#region Properties

/// <summary> /// Идентифкатор временной зоны. /// </summary> public int TimeZoneID { get; }

/// <summary> /// Смещение врменной зоны. /// </summary> public TimeSpan TimeZoneUtcOffset { get; }

/// <summary> /// Имя временной зоны. /// </summary> public string? TimeZoneShortName { get; }

/// <summary> /// Код временной зоны. /// </summary> public string? TimeZoneCodeName { get; }

#endregion }

Получение информации о календаре роли

При помощи функции GetRoleCalendarInfoAsync можно получить информацию о календаре для роли с указанным roleID.

/// <summary> /// Возвращает календарь для роли. /// </summary> /// <param name="roleID">Идентификатор роли.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Возвращает календарь для роли или значение <see langword="null"/>, если его не удалось определить.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<CalendarInfo> GetRoleCalendarInfoAsync( Guid roleID, CancellationToken cancellationToken = default);

Асинхронная функция возвращает объект типа CalendarInfo.

public class CalendarInfo { #region Constructors

public CalendarInfo(Guid calendarID, string calendarName, int? calendarIntID) { this.CalendarID = calendarID; this.CalendarName = calendarName; this.CalendarIntID = calendarIntID; }

#endregion

#region Properties

/// <summary> /// Идентификатор карточки календаря /// </summary> public Guid CalendarID { get; }

/// <summary> /// Имя календаря /// </summary> public string CalendarName { get; }

/// <summary> /// Целочисленный идентификатор календаря /// </summary> public int? CalendarIntID { get; }

#endregion }

Получение информации о временной зоне по умолчанию

При помощи функции GetDefaultTimeZoneInfoAsync можно получить информацию о временной зоне по умолчанию.

/// <summary> /// Возвращает информацию о временной зоне по умолчанию. /// </summary> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Информация о временной зоне или <see langword="null"/>, если карточка временных зон не существует.</returns> Task<TimeZoneInfo?> GetDefaultTimeZoneInfoAsync(CancellationToken cancellationToken = default);

Асинхронная функция возвращает объект типа TimeZoneInfo для временной зоны по умолчанию.

public class TimeZoneInfo { #region Constructors

public TimeZoneInfo(int timeZoneID, TimeSpan utcOffset, string? shortName, string? codeName) { this.TimeZoneID = timeZoneID; this.TimeZoneUtcOffset = utcOffset; this.TimeZoneShortName = shortName; this.TimeZoneCodeName = codeName; }

public TimeZoneInfo(int timeZoneID, int utcOffsetMinutes, string? shortName, string? codeName) { this.TimeZoneID = timeZoneID; this.TimeZoneUtcOffset = TimeSpan.FromMinutes(utcOffsetMinutes); this.TimeZoneShortName = shortName; this.TimeZoneCodeName = codeName; }

#endregion

#region Properties

/// <summary> /// Идентифкатор временной зоны. /// </summary> public int TimeZoneID { get; }

/// <summary> /// Смещение врменной зоны. /// </summary> public TimeSpan TimeZoneUtcOffset { get; }

/// <summary> /// Имя временной зоны. /// </summary> public string? TimeZoneShortName { get; }

/// <summary> /// Код временной зоны. /// </summary> public string? TimeZoneCodeName { get; }

#endregion }

Получение информации о календаре по умолчанию

При помощи функции GetDefaultCalendarInfoAsync можно получить инорфмацию о календаре по умолчанию.

/// <summary> /// Возвращает информацию о календаре по умолчанию. /// </summary> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Информация о календаре по умолчанию или значение <see langword="null"/>, если календарь по умолчанию не найден.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task<CalendarInfo> GetDefaultCalendarInfoAsync(CancellationToken cancellationToken = default);

Асинхронная функция возвращает объект типа CalendarInfo.

public class CalendarInfo { #region Constructors

public CalendarInfo(Guid calendarID, string calendarName, int? calendarIntID) { this.CalendarID = calendarID; this.CalendarName = calendarName; this.CalendarIntID = calendarIntID; }

#endregion

#region Properties

/// <summary> /// Идентификатор карточки календаря /// </summary> public Guid CalendarID { get; }

/// <summary> /// Имя календаря /// </summary> public string CalendarName { get; }

/// <summary> /// Целочисленный идентификатор календаря /// </summary> public int? CalendarIntID { get; }

#endregion }

Получение информации об определённом календаре

При помощи функции GetCalendarInfoAsync можно получить инорфмацию о календаре с идентификатором calendarIntID.

/// <summary> /// Возвращает информацию о календаре. /// </summary> /// <param name="calendarIntID">Целочисленный идентификатор календаря.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Информация о календаре или значение <see langword="null"/>, если календарь не найден.</returns> Task<CalendarInfo> GetCalendarInfoAsync( int calendarIntID, CancellationToken cancellationToken = default);

Асинхронная функция возвращает объект типа CalendarInfo.

public class CalendarInfo { #region Constructors

public CalendarInfo(Guid calendarID, string calendarName, int? calendarIntID) { this.CalendarID = calendarID; this.CalendarName = calendarName; this.CalendarIntID = calendarIntID; }

#endregion

#region Properties

/// <summary> /// Идентификатор карточки календаря /// </summary> public Guid CalendarID { get; }

/// <summary> /// Имя календаря /// </summary> public string CalendarName { get; }

/// <summary> /// Целочисленный идентификатор календаря /// </summary> public int? CalendarIntID { get; }

#endregion }

Получение информации обо всех календарях в системе

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

/// <summary> /// Возвращает список с информацией обо всех календарях в системе. /// </summary> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Список с информацией обо всех календарях в системе.</returns> Task<List<CalendarInfo>> GetAllCalendarInfosAsync(CancellationToken cancellationToken = default);

Асинхронная функция возвращает список объектов типа CalendarInfo.

public class CalendarInfo { #region Constructors

public CalendarInfo(Guid calendarID, string calendarName, int? calendarIntID) { this.CalendarID = calendarID; this.CalendarName = calendarName; this.CalendarIntID = calendarIntID; }

#endregion

#region Properties

/// <summary> /// Идентификатор карточки календаря /// </summary> public Guid CalendarID { get; }

/// <summary> /// Имя календаря /// </summary> public string CalendarName { get; }

/// <summary> /// Целочисленный идентификатор календаря /// </summary> public int? CalendarIntID { get; }

#endregion }

Служебные методы

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

Перестраивание календаря

При помощи функции RebuildCalendarAsync можно вызвать пересчёт календаря аналогично тому, как календарь пересчитывается из карточки.

/// <summary> /// Выполняет перестроение календаря на основании указанных настроек, в т.ч. списка исключений. /// </summary> /// <param name="operationGuid">Идентификатор операции.</param> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="rebuildIndexes">Признак необходимости перестроения индексов в календарях.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Асинхронная задача.</returns> /// <exception cref="ValidationException">Ошибка при получении данных с сервера, если метод вызван на клиенте.</exception> Task RebuildCalendarAsync( Guid operationGuid, Guid calendarCardID, bool rebuildIndexes = false, CancellationToken cancellationToken = default);

Функция реализована с использованием API операций и принимает параметр operationGuid типа Guid - ID операции, а так же идентификатор карточки календаря и признак необходимости перестроения индексов.

Warning

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

Проверка календаря на наличие ошибок

При помощи функции ValidateCalendarAsync можно проверить календарь на наличие ошибок.

/// <summary> /// Проверяет календарь на отсутствие пропусков между квантами. Непредвиденные ошибки при выполнении на клиенте возвращаются /// в объекте <see cref="ValidationResult"/>, а при выполнении на сервере - выбрасываются в виде исключений. /// </summary> /// <param name="calendarCardID">Идентификатор карточки календаря.</param> /// <param name="cancellationToken">Объект, посредством которого можно отменить асинхронную задачу.</param> /// <returns>Результат валидации.</returns> Task<ValidationResult> ValidateCalendarAsync( Guid calendarCardID, CancellationToken cancellationToken = default);

Функция принимает идентификатор карточки календаря, который необходимо проверить на наличие ошибок. На выходе идёт объект типа ValidationResult. Непредвиденные ошибки при выполнении на клиенте возвращаются в нём, а при выполнении на сервере - выбрасываются в виде исключений.

Применение календаря на больших выборках/отчётах

Для больших выборок или отчётов использовать хранимые функции не целесообразно. Для таких случаев целесообразнее общаться с таблицей календаря напрямую.

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

SELECT (([cqu2].[QuantNumber] - [cqu1].[QuantNumber])/4.0) AS [WorkHoursLeft], * FROM [TASKS] AS [tsk] WITH(NOLOCK) OUTER APPLY ( SELECT TOP (1) [q].[QuantNumber] FROM [CalendarQuants] AS [q] WITH (NOLOCK) INNER JOIN [CalendarSettings] AS [cs] WITH (NOLOCK) ON [q].[ID] = [cs].[CalendarID] WHERE [cs].[ID] = [tsk].[CalendarID] AND [q].[StartTime] <= [tsk].[Created] ORDER BY [q].[StartTime] DESC ) AS [cqu1] outer apply ( SELECT TOP (1) [q1].[QuantNumber] FROM [CalendarQuants] AS [q1] WITH (NOLOCK) INNER JOIN [CalendarSettings] AS [cs] WITH (NOLOCK) ON [q1].[ID] = [cs].[CalendarID] WHERE [cs].[ID] = [tsk].[CalendarID] AND [q1].[StartTime] <= [tsk].[Planned] ORDER BY [q1].[StartTime] DESC ) AS [cqu2]

Warning

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

Неправильный запрос

SELECT (([cqu2].[QuantNumber] - [cqu1].[QuantNumber])/4.0) AS [WorkHoursLeft], * FROM [Tasks] AS [tsk] WITH(NOLOCK) LEFT JOIN [CalendarQuants] AS [cqu1] WITH(NOLOCK) ON [cqu1].[StartTime] <= [tsk].[Created] AND [cqu1].[EndTime] > [tsk].[Created] LEFT JOIN [CalendarQuants] AS [cqu2] WITH(NOLOCK) ON [cqu2].[StartTime] <= [tsk].[Planned] AND [cqu2].[EndTime] > [tsk].[Planned]

Причина лучшей производительности первого варианта

И в первом и во втором случае используется поиск по кластерному индексу:

--Первый вариант Clustered Index Seek( OBJECT:([tessa].[dbo].[CalendarQuants].[idx_CalendarQuants_StartTimeUTCEndTimeUTC] AS [q]), SEEK:([q].[StartTime] <= [tessa].[dbo].[Tasks].[Planned] as [tsk].[Planned]) ORDERED FORWARD)

--Второй вариант Clustered Index Seek( OBJECT:([tessa].[dbo].[CalendarQuants].[idx_CalendarQuants_StartTimeUTCEndTimeUTC] AS [cqu2]), SEEK:([cqu2].[StartTime] <= [tessa].[dbo].[Tasks].[Planned] as [tsk].[Planned]), WHERE:([tessa].[dbo].[CalendarQuants].[EndTime] as [cqu2].[EndTime]>[tessa].[dbo].[Tasks].[Planned] as [tsk].[Planned]) ORDERED FORWARD)

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

Back to top