Как объединить смежные периоды в таблице значений 1С 7.7 при совпадении условий

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

В процессе разработки в среде 1С:Предприятие 7.7 программисты часто сталкиваются с задачами обработки интервальных данных. Классический пример такой задачи — подготовка сведений о стаже сотрудников для регламентированной отчетности (ранее СЗВ-СТАЖ, ныне подраздел 1.2 формы ЕФС-1). При заполнении таблицы переводов за год могут возникать ситуации, когда у сотрудника оформлено несколько последовательных кадровых документов, но его должность, класс условий труда и другие параметры стажа не менялись. Чтобы отчет выглядел корректно и не содержал лишних строк, смежные периоды необходимо «схлопнуть» (объединить) в один непрерывный интервал.

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

Представим, что у нас есть исходная таблица значений со следующими колонками: ДатаНач (дата начала периода), ДатаКон (дата окончания периода) и Профессия (или Должность). Наша цель — объединить строки, которые идут друг за другом стык в стык (например, когда дата начала следующего периода равна дате окончания предыдущего плюс 1 день) при условии совпадения профессии.

Вариант 1. Простой алгоритм перебора с сортировкой

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

Проанализируем этот код:


// Предположим, что ТЗ уже создана и заполнена данными
// ТЗ.НоваяКолонка("НачДата", "Дата");
// ТЗ.НоваяКолонка("КонДата", "Дата");
// ТЗ.НоваяКолонка("Должность", "Строка");

// 1. Сортируем таблицу по возрастанию даты начала и должности
ТЗ.Сортировать("+НачДата,Должность");

КвоСтр = ТЗ.КоличествоСтрок(); 
ТекСтр = 1; 

// 2. Запускаем обход таблицы значений
Пока ТекСтр < КвоСтр Цикл
    ТЗ.ПолучитьСтрокуПоНомеру(ТекСтр);
    
    ПоискСтр = ТекСтр + 1; 
    Пока ПоискСтр <= КвоСтр Цикл
        // Проверяем совпадение должности
        Если ТЗ.ПолучитьЗначение(ПоискСтр, "Должность") = ТЗ.Должность Тогда
            // Проверяем, идет ли период стык-в-стык (разница ровно в 1 день)
            Если ТЗ.ПолучитьЗначение(ПоискСтр, "НачДата") = ТЗ.КонДата + 1 Тогда
                // Обновляем дату окончания текущего периода
                ТЗ.КонДата = ТЗ.ПолучитьЗначение(ПоискСтр, "КонДата");
                
                // Удаляем поглощенную строку и уменьшаем общий счетчик
                ТЗ.УдалитьСтроку(ПоискСтр); 
                КвоСтр = КвоСтр - 1;
                
                // Важно: не увеличиваем ПоискСтр, так как на место удаленной строки встала следующая
                Продолжить;
            КонецЕсли;
        КонецЕсли;
        
        ПоискСтр = ПоискСтр + 1;
    КонецЦикла;
    
    ТекСтр = ТекСтр + 1;
КонецЦикла;

Этот алгоритм вполне работоспособен на небольших объемах данных. Однако он имеет существенный недостаток — квадратичную сложность O(N²) из-за вложенного цикла. Если в таблице будет несколько тысяч строк (например, при расчете стажа по крупному предприятию), система может заметно зависнуть. Кроме того, этот код не учитывает ситуации, когда периоды могут частично перекрывать друг друга (например, при внутреннем совместительстве на одной должности).

Вариант 2. Оптимизированный линейный алгоритм слияния интервалов (O(N log N))

Для повышения производительности мы можем применить классический алгоритм слияния интервалов (Merge Intervals). Вместо удаления строк из исходной таблицы на лету, что замедляет работу СУБД-движка 1С 7.7, мы будем переносить объединенные интервалы в новую (результирующую) таблицу значений. Этот способ работает за один проход по исходной отсортированной таблице.

Разберем этот метод по шагам:

  1. Создаем новую результирующую таблицу значений ТЗ_Результат с аналогичной структурой колонок.
  2. Сортируем исходную таблицу ТЗ по полям Должность и НачДата.
  3. Переносим первую строку из исходной таблицы в результирующую в качестве стартовой точки.
  4. Начиная со второй строки исходной таблицы, сравниваем ее параметры с последней строкой в ТЗ_Результат.
  5. Если должность совпадает и дата начала текущей строки меньше или равна дате окончания в результате плюс 1 день (т.е. интервалы пересекаются или стыкуются), мы просто расширяем дату окончания в строке-результате.
  6. Если условия стыковки не выполняются, мы добавляем текущую строку в ТЗ_Результат как новый самостоятельный интервал.

Давайте посмотрим на программную реализацию этого алгоритма:


// Создаем результирующую таблицу
ТЗ_Результат = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Результат.НоваяКолонка("НачДата", "Дата");
ТЗ_Результат.НоваяКолонка("КонДата", "Дата");
ТЗ_Результат.НоваяКолонка("Должность", "Строка");

// Сортируем исходную таблицу
ТЗ.Сортировать("Должность,+НачДата");

КвоСтрок = ТЗ.КоличествоСтрок();
Если КвоСтрок > 0 Тогда
    // Копируем первую строку в результат
    ТЗ.ПолучитьСтрокуПоНомеру(1);
    ТЗ_Результат.НоваяСтрока();
    ТЗ_Результат.НачДата = ТЗ.НачДата;
    ТЗ_Результат.КонДата = ТЗ.КонДата;
    ТЗ_Результат.Должность = ТЗ.Должность;
    
    // Обходим оставшиеся строки
    Для Ном = 2 По КвоСтрок Цикл
        ТЗ.ПолучитьСтрокуПоНомеру(Nom);
        
        // Получаем параметры последней строки в результирующей таблице
        ИндексРезультата = ТЗ_Результат.КоличествоСтрок();
        ПослДолжность = ТЗ_Результат.ПолучениеЗначения(ИндексРезультата, "Должность");
        ПослКонДата   = ТЗ_Результат.ПолучениеЗначения(ИндексРезультата, "КонДата");
        
        // Проверяем условия объединения
        Если (ВРЕС(СокрЛП(ТЗ.Должность)) = ВРЕС(СокрЛП(ПослДолжность))) И (ТЗ.НачДата <= ПослКонДата + 1) Тогда
            // Если текущая дата окончания больше сохраненной в результате, расширяем интервал
            Если ТЗ.КонДата > ПослКонДата Тогда
                ТЗ_Результат.УстановитьЗначение(ИндексРезультата, "КонДата", ТЗ.КонДата);
            КонецЕсли;
        Иначе
            // Добавляем новый интервал
            ТЗ_Результат.НоваяСтрока();
            ТЗ_Результат.НачДата = ТЗ.НачДата;
            ТЗ_Результат.КонДата = ТЗ.КонДата;
            ТЗ_Результат.Должность = ТЗ.Должность;
        КонецЕсли;
    КонецЦикла;
КонецЕсли;

// Теперь ТЗ_Результат содержит свернутые по периодам данные

Этот алгоритм работает за линейное время O(N) после выполнения сортировки, что делает его чрезвычайно эффективным на больших базах данных.

Вариант 3. Учет выходных дней и технологических разрывов

В реальной практике кадрового учета часто возникает методологический вопрос: нужно ли объединять стаж, если сотрудник уволился в пятницу, а в понедельник снова был принят на ту же должность? Формально между датами есть разрыв в 2 дня (выходные), но с точки зрения подсчета стажа для пенсионных органов такие разрывы часто разрешается игнорировать и объединять периоды в одну строку.

Чтобы наш алгоритм мог гибко справляться с такими нюансами, введем понятие допустимого разрыва стажа в днях. Мы добавим числовую переменную ДопустимыйРазрыв. Если разрыв между датами укладывается в этот интервал, периоды склеиваются.

Рассмотрим модифицированное условие склейки:


ДопустимыйРазрыв = 2; // Разрешаем склеивать периоды с разрывом до 2 дней включительно

// Внутри цикла обхода таблицы заменяем проверку условия стыковки:
Если (ВРЕС(СокрЛП(ТЗ.Должность)) = ВРЕС(СокрЛП(ПослДолжность))) 
    И (ТЗ.НачДата <= ПослКонДата + 1 + ДопустимыйРазрыв) Тогда
    
    // Склеиваем интервалы
    Если ТЗ.КонДата > ПослКонДата Тогда
        ТЗ_Результат.УстановитьЗначение(ИндексРезультата, "КонДата", ТЗ.КонДата);
    КонецЕсли;
Иначе
    // Создаем новую строку
    ...
КонецЕсли;

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

Вариант 4. Повышение производительности с помощью "ИндексированнойТаблицы" (компонента 1С++)

Если в вашей конфигурации 1С 7.7 используется внешняя компонента 1С++, вы можете задействовать объект ИндексированнаяТаблица. Использование индексов позволяет выполнять поиск дубликатов и смежных записей практически мгновенно, минуя медленный стандартный поиск по всей таблице значений.

Пример инициализации и индексации таблицы значений:


// Создаем объект индексированной таблицы значений из ВК 1С++
ИндТЗ = СоздатьОбъект("ИндексированнаяТаблица");
ИндТЗ.НоваяКолонка("НачДата", "Дата");
ИндТЗ.НоваяКолонка("КонДата", "Дата");
ИндТЗ.НоваяКолонка("Должность", "Строка");

// Заполняем данными...

// Создаем индекс по должности и дате начала для быстрой сортировки и поиска
ИндТЗ.ДобавитьИндекс("ИндексДолжДаты", "Должность, НачДата");
ИндТЗ.Сортировать("ИндексДолжДаты");

Использование индексированных таблиц сводит к минимуму время выполнения ресурсоемких операций в оперативной памяти, что особенно критично для файловых баз данных 1С 7.7 больших объемов.

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

← На главную