При разработке кадровых или табельных отчетов в самописных конфигурациях без внедренной БСП часто возникает задача разбора «горизонтальных» таблиц (так называемой «шахматки»). В таких таблицах колонки именуются днями месяца (например, от День1 до День31), а в ячейках хранятся числовые показатели отклонений (часы отсутствия, опоздания) и буквенные обозначения (например, символ "В" — выходной день).
Рассмотрим типовую проблему: необходимо преобразовать горизонтальную таблицу значений в плоский список интервалов («Отметки»), содержащий колонки ФИО, ДатаНачала и ДатаОкончания. При этом выходные дни, находящиеся внутри периода отсутствия, должны поглощаться интервалом, а одиночные выходные или выходные за пределами отклонений не должны образовывать ложных записей.
Проанализируем структуру входных данных. В исходной таблице значений «Визиты» содержатся строки сотрудников. Каждая строка содержит колонки с днями месяца:
"В" обозначает плановый выходной день.Сложность заключается в логике объединения: если между рабочими днями отклонений идут подряд один или несколько выходных (например, День1 - 8, День2 - 8, День3 - В, День4 - В, День5 - 8), этот отрезок должен сформировать единый диапазон с 1 по 5 число. Однако если после выходного не последовало отклонения (например, День10 - 8, День11 - В, а День12 пустой), то выходной не должен удлинять интервал: на выходе должен остаться отрезок только за 10 число.
В классической теории баз данных подобная задача формулируется как Gaps and Islands («Острова и промежутки») с условием поглощения разделителей. Рассмотрим эффективные способы ее решения в платформе 1С:Предприятие.
Встроенный язык 1С не поддерживает аналитические оконные функции SQL вроде LAG или LEAD. Попытка построить интервалы запросом на самосоединениях для горизонтальной таблицы приводит к громоздким конструкциям и замедлению работы. Наиболее производительным решением является линейный алгоритмический обход колонок сотрудника за один цикл сложности O(N).
Разберем по шагам алгоритм с хранением двух указателей дат:
ДатаНачалаИнтервала и ДатаПоследнегоОтклонения как пустые даты.День(КонецМесяца(ДатаОтчета)). Это защитит алгоритм от ошибок в месяцах, где меньше 31 дня.ДатаНачалаИнтервала не заполнена — фиксируем ее текущей датой.
- Значение ДатаПоследнегоОтклонения в любом случае обновляем на текущую дату.
"В"):
- Если цепочка уже открыта (ЗначениеЗаполнено(ДатаНачалаИнтервала)), продолжаем обход. Выходной день потенциально входит в интервал, но ДатаПоследнегоОтклонения не сдвигается!
- Если интервал еще не был начат, выходной просто игнорируется.
ДатаНачалаИнтервала до ДатаПоследнегоОтклонения.
- Сбрасываем обе даты.
Посмотрим на пример реализации алгоритма на встроенном языке 1С:
// Предполагается, что Объект.ДатаОтчета задает анализируемый период (месяц)
ГодОтчета = Год(Объект.ДатаОтчета);
МесяцОтчета = Месяц(Объект.ДатаОтчета);
ДнейВМесяце = День(КонецМесяца(Объект.ДатаОтчета));
Отметки.Очистить();
Для Каждого СтрВизит Из Визиты Цикл
ДатаНачалаИнтервала = '00010101';
ДатаПоследнегоОтклонения = '00010101';
Для Дн = 1 По ДнейВМесяце Цикл
ИмяКолонки = "День" + Формат(Дн, "ЧГ=0");
ЗначениеДня = СтрВизит[ИмяКолонки];
ТекущаяДата = Дата(ГодОтчета, МесяцОтчета, Дн);
ЭтоЧисло = (ТипЗнч(ЗначениеДня) = Тип("Число") И ЗначениеДня > 0);
ЭтоВыходной = (ВРег(СокрЛП(Строка(ЗначениеДня))) = "В");
Если ЭтоЧисло Тогда
Если НЕ ЗначениеЗаполнено(ДатаНачалаИнтервала) Тогда
ДатаНачалаИнтервала = ТекущаяДата;
КонецЕсли;
ДатаПоследнегоОтклонения = ТекущаяДата;
ИначеЕсли ЭтоВыходной Тогда
// Если интервал начат, продолжаем его накапливать.
// Но ДатаПоследнегоОтклонения не сдвигается, пока не встретится рабочее отклонение.
Если НЕ ЗначениеЗаполнено(ДатаНачалаИнтервала) Тогда
// Одиночный выходной в начале или вне интервалов игнорируем
Продолжить;
КонецЕсли;
Иначе
// Встретили рабочий день без отклонений (разрыв периода)
Если ЗначениеЗаполнено(ДатаНачалаИнтервала) Тогда
НоваяСтрока = Отметки.Добавить();
НоваяСтрока.ФИО = СтрВизит.ФИО;
НоваяСтрока.ДатаНачала = ДатаНачалаИнтервала;
НоваяСтрока.ДатаОкончания = ДатаПоследнегоОтклонения;
ДатаНачалаИнтервала = '00010101';
ДатаПоследнегоОтклонения = '00010101';
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Закрываем период, если месяц закончился отклонением
Если ЗначениеЗаполнено(ДатаНачалаИнтервала) Тогда
НоваяСтрока = Отметки.Добавить();
НоваяСтрока.ФИО = СтрВизит.ФИО;
НоваяСтрока.ДатаНачала = ДатаНачалаИнтервала;
НоваяСтрока.ДатаОкончания = ДатаПоследнегоОтклонения;
КонецЕсли;
КонецЦикла;
Рассмотрим альтернативный подход, который часто предлагают разработчики баз данных: использование транзитивных замыканий (графов) в языке запросов. Идея заключается в представлении каждого дня как элементарного отрезка фиксированной длины (1 день) и последующем каскадном объединении смежных отрезков через внутренние соединения.
Выясним причину, почему данный метод не всегда оптимален в 1С: для склейки интервалов длиной в месяц требуется последовательно соединять таблицы замыканий (длины 1, 2, 4, 8, 16, 32 дня). Ниже приведен фрагмент структуры запроса, объединяющего непрерывные дуги:
ВЫБРАТЬ
Таб.Ссылка КАК Ссылка,
Таб.ДатаНачала КАК НачалоДуги,
Таб.ДатаКонца КАК КонецДуги
ПОМЕСТИТЬ ЗамыканияДлины1
ИЗ
ВременнаяТаблицаДней КАК Таб;
ВЫБРАТЬ РАЗЛИЧНЫЕ
ПерваяДуга.НачалоДуги,
ВтораяДуга.КонецДуги,
ПерваяДуга.Ссылка
ПОМЕСТИТЬ ЗамыканияДлины2
ИЗ
ЗамыканияДлины1 КАК ПерваяДуга
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ЗамыканияДлины1 КАК ВтораяДуга
ПО ПерваяДуга.КонецДуги = ВтораяДуга.НачалоДуги
И ПерваяДуга.Ссылка = ВтораяДуга.Ссылка
ОБЪЕДИНИТЬ
ВЫБРАТЬ
Таб.ДатаНачала,
Таб.ДатаКонца,
Таб.Ссылка
ИЗ
ЗамыканияДлины1 КАК Таб;
Такой подход полезен при сложной обработке данных исключительно средствами СУБД, однако для прикладных задач внутри клиентской обработки алгоритм линейного перебора (Решение 1) работает в десятки раз быстрее, потребляет минимум оперативной памяти и прозрачен в отладке.
" 8". Чтобы код не пропустил рабочие дни, используйте предварительное преобразование через Число() с перехватом исключений или функции строковой очистки.ДатаОкончания предыдущей строки с ДатаНачала - 86400 текущей строки.Дата (состав даты: ЧастиДаты.Дата), что ускорит последующие отборы и вывод в отчет.