Как сгруппировать посуточную таблицу значений в периоды с учетом выходных дней в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке кадровых или табельных отчетов в самописных конфигурациях без внедренной БСП часто возникает задача разбора «горизонтальных» таблиц (так называемой «шахматки»). В таких таблицах колонки именуются днями месяца (например, от День1 до День31), а в ячейках хранятся числовые показатели отклонений (часы отсутствия, опоздания) и буквенные обозначения (например, символ "В" — выходной день).

Рассмотрим типовую проблему: необходимо преобразовать горизонтальную таблицу значений в плоский список интервалов («Отметки»), содержащий колонки ФИО, ДатаНачала и ДатаОкончания. При этом выходные дни, находящиеся внутри периода отсутствия, должны поглощаться интервалом, а одиночные выходные или выходные за пределами отклонений не должны образовывать ложных записей.

Анализ проблемы и структура исходных данных

Проанализируем структуру входных данных. В исходной таблице значений «Визиты» содержатся строки сотрудников. Каждая строка содержит колонки с днями месяца:

  1. Числовые значения больше нуля (например, 8) означают факт отклонения от графика.
  2. Символ "В" обозначает плановый выходной день.
  3. Пустые значения означают отсутствие отклонений (сотрудник отработал штатно).

Сложность заключается в логике объединения: если между рабочими днями отклонений идут подряд один или несколько выходных (например, День1 - 8, День2 - 8, День3 - В, День4 - В, День5 - 8), этот отрезок должен сформировать единый диапазон с 1 по 5 число. Однако если после выходного не последовало отклонения (например, День10 - 8, День11 - В, а День12 пустой), то выходной не должен удлинять интервал: на выходе должен остаться отрезок только за 10 число.

В классической теории баз данных подобная задача формулируется как Gaps and Islands («Острова и промежутки») с условием поглощения разделителей. Рассмотрим эффективные способы ее решения в платформе 1С:Предприятие.

Решение 1: Линейный обход с двумя указателями (Рекомендуемый)

Встроенный язык 1С не поддерживает аналитические оконные функции SQL вроде LAG или LEAD. Попытка построить интервалы запросом на самосоединениях для горизонтальной таблицы приводит к громоздким конструкциям и замедлению работы. Наиболее производительным решением является линейный алгоритмический обход колонок сотрудника за один цикл сложности O(N).

Разберем по шагам алгоритм с хранением двух указателей дат:

  1. Инициализируем переменные ДатаНачалаИнтервала и ДатаПоследнегоОтклонения как пустые даты.
  2. В цикле обходим дни месяца от 1 до День(КонецМесяца(ДатаОтчета)). Это защитит алгоритм от ошибок в месяцах, где меньше 31 дня.
  3. Проверяем тип и значение ячейки. Для надежности приводим данные к универсальному виду, исключая строковые числа.
  4. Если обнаружено отклонение (число больше нуля): - Если ДатаНачалаИнтервала не заполнена — фиксируем ее текущей датой. - Значение ДатаПоследнегоОтклонения в любом случае обновляем на текущую дату.
  5. Если встретился выходной ("В"): - Если цепочка уже открыта (ЗначениеЗаполнено(ДатаНачалаИнтервала)), продолжаем обход. Выходной день потенциально входит в интервал, но ДатаПоследнегоОтклонения не сдвигается! - Если интервал еще не был начат, выходной просто игнорируется.
  6. Если день пустой (отклонений нет): - Если была открыта цепочка, фиксируем интервал в результирующей таблице: от ДатаНачалаИнтервала до ДатаПоследнегоОтклонения. - Сбрасываем обе даты.
  7. После завершения цикла по дням месяца обязательно проверяем граничное условие: если месяц завершился на открытом интервале, записываем остаток периода.

Посмотрим на пример реализации алгоритма на встроенном языке 1С:


// Предполагается, что Объект.ДатаОтчета задает анализируемый период (месяц)
ГодОтчета   = Год(Объект.ДатаОтчета);
МесяцОтчета = Месяц(Объект.ДатаОтчета);
ДнейВМесяце = День(КонецМесяца(Объект.ДатаОтчета));

Отметки.Очистить();

Для Каждого СтрВизит Из Визиты Цикл

    ДатаНачалаИнтервала     = '00010101';
    ДатаПоследнегоОтклонения = '00010101';

    Для Дн = 1 По ДнейВМесяце Цикл

        ИмяКолонки = "День" + Формат(Дн, "ЧГ=0");
        ЗначениеДня = СтрВизит[ИмяКолонки];
        ТекущаяДата = Дата(ГодОтчета, МесяцОтчета, Дн);

        ЭтоЧисло = (ТипЗнч(ЗначениеДня) = Тип("Число") И ЗначениеДня > 0);
        ЭтоВыходной = (ВРег(СокрЛП(Строка(ЗначениеДня))) = "В");

        Если ЭтоЧисло Тогда
            Если НЕ ЗначениеЗаполнено(ДатаНачалаИнтервала) Тогда
                ДатаНачалаИнтервала = ТекущаяДата;
            КонецЕсли;
            ДатаПоследнегоОтклонения = ТекущаяДата;

        ИначеЕсли ЭтоВыходной Тогда
            // Если интервал начат, продолжаем его накапливать.
            // Но ДатаПоследнегоОтклонения не сдвигается, пока не встретится рабочее отклонение.
            Если НЕ ЗначениеЗаполнено(ДатаНачалаИнтервала) Тогда
                // Одиночный выходной в начале или вне интервалов игнорируем
                Продолжить;
            КонецЕсли;

        Иначе
            // Встретили рабочий день без отклонений (разрыв периода)
            Если ЗначениеЗаполнено(ДатаНачалаИнтервала) Тогда
                НоваяСтрока = Отметки.Добавить();
                НоваяСтрока.ФИО = СтрВизит.ФИО;
                НоваяСтрока.ДатаНачала = ДатаНачалаИнтервала;
                НоваяСтрока.ДатаОкончания = ДатаПоследнегоОтклонения;

                ДатаНачалаИнтервала = '00010101';
                ДатаПоследнегоОтклонения = '00010101';
            КонецЕсли;
        КонецЕсли;

    КонецЦикла;

    // Закрываем период, если месяц закончился отклонением
    Если ЗначениеЗаполнено(ДатаНачалаИнтервала) Тогда
        НоваяСтрока = Отметки.Добавить();
        НоваяСтрока.ФИО = СтрВизит.ФИО;
        НоваяСтрока.ДатаНачала = ДатаНачалаИнтервала;
        НоваяСтрока.ДатаОкончания = ДатаПоследнегоОтклонения;
    КонецЕсли;

КонецЦикла;

Решение 2: Использование транзитивного замыкания через запрос

Рассмотрим альтернативный подход, который часто предлагают разработчики баз данных: использование транзитивных замыканий (графов) в языке запросов. Идея заключается в представлении каждого дня как элементарного отрезка фиксированной длины (1 день) и последующем каскадном объединении смежных отрезков через внутренние соединения.

Выясним причину, почему данный метод не всегда оптимален в 1С: для склейки интервалов длиной в месяц требуется последовательно соединять таблицы замыканий (длины 1, 2, 4, 8, 16, 32 дня). Ниже приведен фрагмент структуры запроса, объединяющего непрерывные дуги:


ВЫБРАТЬ
    Таб.Ссылка КАК Ссылка,
    Таб.ДатаНачала КАК НачалоДуги,
    Таб.ДатаКонца КАК КонецДуги
ПОМЕСТИТЬ ЗамыканияДлины1
ИЗ
    ВременнаяТаблицаДней КАК Таб;

ВЫБРАТЬ РАЗЛИЧНЫЕ
    ПерваяДуга.НачалоДуги,
    ВтораяДуга.КонецДуги,
    ПерваяДуга.Ссылка
ПОМЕСТИТЬ ЗамыканияДлины2
ИЗ
    ЗамыканияДлины1 КАК ПерваяДуга
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ЗамыканияДлины1 КАК ВтораяДуга
        ПО ПерваяДуга.КонецДуги = ВтораяДуга.НачалоДуги
        И ПерваяДуга.Ссылка = ВтораяДуга.Ссылка
ОБЪЕДИНИТЬ
ВЫБРАТЬ
    Таб.ДатаНачала,
    Таб.ДатаКонца,
    Таб.Ссылка
ИЗ
    ЗамыканияДлины1 КАК Таб;

Такой подход полезен при сложной обработке данных исключительно средствами СУБД, однако для прикладных задач внутри клиентской обработки алгоритм линейного перебора (Решение 1) работает в десятки раз быстрее, потребляет минимум оперативной памяти и прозрачен в отладке.

Важные практические нюансы

  1. Проверка на строковый ввод: При ручной вбивке или загрузке из внешних файлов Excel значения часто попадают в таблицу значений со строковым типом " 8". Чтобы код не пропустил рабочие дни, используйте предварительное преобразование через Число() с перехватом исключений или функции строковой очистки.
  2. Переходы через границу месяца: Горизонтальная таблица всегда ограничена одним отчетным месяцем. Если отсутствие началось в конце одного месяца и продолжилось в начале следующего, интервал будет разбит на два. Для получения сквозных периодов результирующую таблицу «Отметки» рекомендуется объединять дополнительным циклом, сопоставляя ДатаОкончания предыдущей строки с ДатаНачала - 86400 текущей строки.
  3. Индексация и структуры: Для обработки сотен сотрудников формируйте результирующие интервалы напрямую в ТЗ с типизированными колонками Дата (состав даты: ЧастиДаты.Дата), что ускорит последующие отборы и вывод в отчет.
← На главную