Календари
Календари¶
Календари - это набор специального инструментария, который позволяет легко решать задачи по управлению и получению информации, связанной с рабочим и нерабочим временем. Календари оперируют датами исключительно в формате 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)
Но в первом варианте на каждое задание происходит ровно одно чтение значения (благодаря топу и индексу построенному по возрастанию), а во втором варианте, для каждого из найденных значений (дата планируемого завершения больше даты начала кванта) проверяется, что дата завершения меньше даты конца кванта - кол-во чтений зависит от размера таблицы календаря, и близости самого старого найденного кванта к концу таблицы.