Как объединить последовательные коды справочника в интервалы в 1С?

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

При формировании аналитических отчетов и выгрузок часто возникает задача компактного представления последовательностей чисел. Например, если в справочнике присутствуют элементы с кодами 1, 2, 3, 4, 5, 7, 8, 9, 12, 15, 16, 17, их требуется свернуть в непрерывные диапазоны: 1-5, 7-9, 12, 15-17. В теории баз данных эта задача известна как «Острова и промежутки» (Gaps and Islands).

Разберем два основных подхода к решению этой задачи в платформе 1С:Предприятие: с помощью чистого запроса по данным информационной базы и программным способом через разбор переданных значений с выводом в отчет на СКД.

Вариант 1. Поиск интервалов с помощью чистого запроса (метод «Островов»)

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

Рассмотрим логику алгоритма по шагам:

  1. Определение левых границ интервалов: число является началом интервала, если в таблице отсутствует число, меньшее его на единицу (Код - 1). Для этого используется левое соединение таблицы самой с собой.
  2. Определение правых границ интервалов: число является концом интервала, если в таблице отсутствует число, большее его на единицу (Код + 1).
  3. Схлопывание диапазонов: соединяем таблицу левых границ с таблицей правых границ по условию ЛеваяГраница <= ПраваяГраница и группируем по левой границе, выбирая минимальное значение правой границы.

Посмотрим на пример реализации запроса:


ВЫБРАТЬ
    Справочник.Код КАК ЧисловойКод
ПОМЕСТИТЬ ВТ_ИсходныеКоды
ИЗ
    Справочник.Номенклатура КАК Справочник
ГДЕ
    НЕ Справочник.ПометкаУдаления
;

////////////////////////////////////////////////////////////////////////////////
// 1. Находим все левые границы диапазонов
ВЫБРАТЬ
    Т.ЧисловойКод КАК ЛеваяГраница
ПОМЕСТИТЬ ВТ_ЛевыеГраницы
ИЗ
    ВТ_ИсходныеКоды КАК Т
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсходныеКоды КАК Т_Предыдущий
        ПО (Т.ЧисловойКод = Т_Предыдущий.ЧисловойКод + 1)
ГДЕ
    Т_Предыдущий.ЧисловойКод ЕСТЬ NULL
;

////////////////////////////////////////////////////////////////////////////////
// 2. Находим все правые границы диапазонов
ВЫБРАТЬ
    Т.ЧисловойКод КАК ПраваяГраница
ПОМЕСТИТЬ ВТ_ПравыеГраницы
ИЗ
    ВТ_ИсходныеКоды КАК Т
        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсходныеКоды КАК Т_Следующий
        ПО (Т.ЧисловойКод = Т_Следующий.ЧисловойКод - 1)
ГДЕ
    Т_Следующий.ЧисловойКод ЕСТЬ NULL
;

////////////////////////////////////////////////////////////////////////////////
// 3. Сопоставляем границы и формируем итоговый интервал
ВЫБРАТЬ
    Левые.ЛеваяГраница КАК НачалоИнтервала,
    МИНИМУМ(Правые.ПраваяГраница) КАК КонецИнтервала,
    ВЫБОР
        КОГДА Левые.ЛеваяГраница = МИНИМУМ(Правые.ПраваяГраница)
            ТОГДА СТРОКА(Левые.ЛеваяГраница)
        ИНАЧЕ СТРОКА(Левые.ЛеваяГраница) + " - " + СТРОКА(МИНИМУМ(Правые.ПраваяГраница))
    КОНЕЦ КАК ПредставлениеИнтервала
ИЗ
    ВТ_ЛевыеГраницы КАК Левые
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПравыеГраницы КАК Правые
        ПО Левые.ЛеваяГраница <= Правые.ПраваяГраница
СГРУППИРОВАТЬ ПО
    Левые.ЛеваяГраница

Вариант 2. Программный разбор строки и передача данных в СКД

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

Разберем причину частой ошибки: при попытке вставить динамический текст с временной таблицей ВТ_Интервалы в запрос схемы компоновки данных возникает ошибка «Таблица не найдена "ВТ_Интервалы"». Это происходит потому, что СКД проверяет синтаксис на этапе инициализации схемы, когда временная таблица еще не создана. Чтобы этого избежать, сформируем ТаблицаЗначений в коде и передадим её в отчет как внешний набор данных.

Реализуем процедуру формирования таблицы интервалов и компоновки отчета в модуле объекта отчета:


Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
    
    СтандартнаяОбработка = Ложь;
    
    // Получаем строку интервалов из параметров компоновки
    Настройки = КомпоновщикНастроек.ПолучитьНастройки();
    ПараметрСтрока = Настройки.ПараметрыДанных.Элементы.Найти("СтрокаИнтервалов");
    
    Если ПараметрСтрока = Неопределено ИЛИ НЕ ЗначениеЗаполнено(ПараметрСтрока.Значение) Тогда
        Возврат;
    КонецЕсли;
    
    // Формируем таблицу соответствия кодов и интервалов
    ТЗ_Интервалы = СформироватьТаблицуИнтервалов(ПараметрСтрока.Значение);
    
    // Передаем таблицу как внешний набор данных
    ВнешниеНаборы = Новый Структура;
    ВнешниеНаборы.Вставить("ТЗ_Интервалы", ТЗ_Интервалы);
    
    КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
    МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки, ДанныеРасшифровки);
    
    ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
    ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, ВнешниеНаборы, ДанныеРасшифровки, Истина);
    
    ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
    ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
    ПроцессорВывода.Вывести(ПроцессорКомпоновки);
    
КонецПроцедуры

Функция СформироватьТаблицуИнтервалов(СтрокаИнтервалов)
    
    ТЗ = Новый ТаблицаЗначений;
    ТЗ.Колонки.Добавить("Код", Новый ОписаниеТипов("Число"));
    ТЗ.Колонки.Добавить("ИмяИнтервала", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(50)));
    
    МассивСтрок = СтрРазделить(СтрокаИнтервалов, ",", Ложь);
    МассивЧисел = Новый Массив;
    
    Для Каждого Стр Из МассивСтрок Цикл
        ТекЗначение = СокрЛП(Стр);
        Если ЗначениеЗаполнено(ТекЗначение) Тогда
            Попытка
                МассивЧисел.Добавить(Число(ТекЗначение));
            Исключение
                // Пропускаем некорректные символы
            КонецПопытки;
        КонецЕсли;
    КонецЦикла;
    
    // Если массив не отсортирован, сортируем значения через список значений
    Список = Новый СписокЗначений;
    Список.ЗагрузитьЗначения(МассивЧисел);
    Список.СортироватьПоЗначению();
    МассивЧисел = Список.ВыгрузитьЗначения();
    
    Если МассивЧисел.Количество() = 0 Тогда
        Возврат ТЗ;
    КонецЕсли;
    
    НачалоИнтервала = МассивЧисел[0];
    ПредыдущееЧисло = МассивЧисел[0];
    ТекущийИнтервалКоды = Новый Массив;
    ТекущийИнтервалКоды.Добавить(НачалоИнтервала);
    
    Для Индекс = 1 По МассивЧисел.ВГраница() Цикл
        ТекЧисло = МассивЧисел[Индекс];
        
        Если ТекЧисло = ПредыдущееЧисло + 1 Тогда
            ТекущийИнтервалКоды.Добавить(ТекЧисло);
        Иначе
            // Закрываем предыдущий интервал
            ЗаписатьИнтервалВТаблицу(ТЗ, ТекущийИнтервалКоды, НачалоИнтервала, ПредыдущееЧисло);
            
            // Начинаем новый интервал
            ТекущийИнтервалКоды.Очистить();
            НачалоИнтервала = ТекЧисло;
            ТекущийИнтервалКоды.Добавить(ТекЧисло);
        КонецЕсли;
        
        ПредыдущееЧисло = ТекЧисло;
    КонецЦикла;
    
    // Записываем последний интервал
    ЗаписатьИнтервалВТаблицу(ТЗ, ТекущийИнтервалКоды, НачалоИнтервала, ПредыдущееЧисло);
    
    Возврат ТЗ;
    
КонецФункции

Процедура ЗаписатьИнтервалВТаблицу(ТЗ, МассивКодов, НачалоИнтервала, КонецИнтервала)
    
    Если НачалоИнтервала = КонецИнтервала Тогда
        Представление = Формат(НачалоИнтервала, "ЧГ=0");
    Иначе
        Представление = Формат(НачалоИнтервала, "ЧГ=0") + " - " + Формат(КонецИнтервала, "ЧГ=0");
    КонецЕсли;
    
    Для Каждого Код Из МассивКодов Цикл
        НоваяСтрока = ТЗ.Добавить();
        НоваяСтрока.Код = Код;
        НоваяСтрока.ИмяИнтервала = Представление;
    КонецЦикла;
    
КонецПроцедуры

Важные рекомендации при работе с интервалами

  1. Форматирование чисел: при преобразовании кодов в строку обязательно используйте параметр формата ЧГ=0 (Формат(Код, "ЧГ=0")). В противном случае платформа автоматически добавит неразрывные пробелы в разряды чисел (например, 1 000 вместо 1000).
  2. Отказ от больших динамических запросов: не рекомендуется формировать временные таблицы через склейку сотен строк ВЫБРАТЬ ... ОБЪЕДИНИТЬ ВСЕ. Это создает избыточную нагрузку на компилятор запросов СУБД и может вызвать ошибку переполнения стека парсера SQL. Передача готовой таблицы значений через параметры запроса или внешний набор данных работает стабильнее и быстрее.
← На главную