При формировании аналитических отчетов и выгрузок часто возникает задача компактного представления последовательностей чисел. Например, если в справочнике присутствуют элементы с кодами 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. Находим все левые границы диапазонов
ВЫБРАТЬ
Т.ЧисловойКод КАК ЛеваяГраница
ПОМЕСТИТЬ ВТ_ЛевыеГраницы
ИЗ
ВТ_ИсходныеКоды КАК Т
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсходныеКоды КАК Т_Предыдущий
ПО (Т.ЧисловойКод = Т_Предыдущий.ЧисловойКод + 1)
ГДЕ
Т_Предыдущий.ЧисловойКод ЕСТЬ NULL
;
////////////////////////////////////////////////////////////////////////////////
// 2. Находим все правые границы диапазонов
ВЫБРАТЬ
Т.ЧисловойКод КАК ПраваяГраница
ПОМЕСТИТЬ ВТ_ПравыеГраницы
ИЗ
ВТ_ИсходныеКоды КАК Т
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ИсходныеКоды КАК Т_Следующий
ПО (Т.ЧисловойКод = Т_Следующий.ЧисловойКод - 1)
ГДЕ
Т_Следующий.ЧисловойКод ЕСТЬ NULL
;
////////////////////////////////////////////////////////////////////////////////
// 3. Сопоставляем границы и формируем итоговый интервал
ВЫБРАТЬ
Левые.ЛеваяГраница КАК НачалоИнтервала,
МИНИМУМ(Правые.ПраваяГраница) КАК КонецИнтервала,
ВЫБОР
КОГДА Левые.ЛеваяГраница = МИНИМУМ(Правые.ПраваяГраница)
ТОГДА СТРОКА(Левые.ЛеваяГраница)
ИНАЧЕ СТРОКА(Левые.ЛеваяГраница) + " - " + СТРОКА(МИНИМУМ(Правые.ПраваяГраница))
КОНЕЦ КАК ПредставлениеИнтервала
ИЗ
ВТ_ЛевыеГраницы КАК Левые
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПравыеГраницы КАК Правые
ПО Левые.ЛеваяГраница <= Правые.ПраваяГраница
СГРУППИРОВАТЬ ПО
Левые.ЛеваяГраница
Если список номеров задается пользователем в виде строки через запятую (например, в диалоге отчета) и его необходимо сопоставить со справочником в схеме компоновки данных, программная обработка во встроенном языке будет более производительной.
Разберем причину частой ошибки: при попытке вставить динамический текст с временной таблицей ВТ_Интервалы в запрос схемы компоновки данных возникает ошибка «Таблица не найдена "ВТ_Интервалы"». Это происходит потому, что СКД проверяет синтаксис на этапе инициализации схемы, когда временная таблица еще не создана. Чтобы этого избежать, сформируем ТаблицаЗначений в коде и передадим её в отчет как внешний набор данных.
Реализуем процедуру формирования таблицы интервалов и компоновки отчета в модуле объекта отчета:
Процедура ПриКомпоновкеРезультата(ДокументРезультат, ДанныеРасшифровки, СтандартнаяОбработка)
СтандартнаяОбработка = Ложь;
// Получаем строку интервалов из параметров компоновки
Настройки = КомпоновщикНастроек.ПолучитьНастройки();
ПараметрСтрока = Настройки.ПараметрыДанных.Элементы.Найти("СтрокаИнтервалов");
Если ПараметрСтрока = Неопределено ИЛИ НЕ ЗначениеЗаполнено(ПараметрСтрока.Значение) Тогда
Возврат;
КонецЕсли;
// Формируем таблицу соответствия кодов и интервалов
ТЗ_Интервалы = СформироватьТаблицуИнтервалов(ПараметрСтрока.Значение);
// Передаем таблицу как внешний набор данных
ВнешниеНаборы = Новый Структура;
ВнешниеНаборы.Вставить("ТЗ_Интервалы", ТЗ_Интервалы);
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
МакетКомпоновки = КомпоновщикМакета.Выполнить(СхемаКомпоновкиДанных, Настройки, ДанныеРасшифровки);
ПроцессорКомпоновки = Новый ПроцессорКомпоновкиДанных;
ПроцессорКомпоновки.Инициализировать(МакетКомпоновки, ВнешниеНаборы, ДанныеРасшифровки, Истина);
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВТабличныйДокумент;
ПроцессорВывода.УстановитьДокумент(ДокументРезультат);
ПроцессорВывода.Вывести(ПроцессорКомпоновки);
КонецПроцедуры
Функция СформироватьТаблицуИнтервалов(СтрокаИнтервалов)
ТЗ = Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("Код", Новый ОписаниеТипов("Число"));
ТЗ.Колонки.Добавить("ИмяИнтервала", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(50)));
МассивСтрок = СтрРазделить(СтрокаИнтервалов, ",", Ложь);
МассивЧисел = Новый Массив;
Для Каждого Стр Из МассивСтрок Цикл
ТекЗначение = СокрЛП(Стр);
Если ЗначениеЗаполнено(ТекЗначение) Тогда
Попытка
МассивЧисел.Добавить(Число(ТекЗначение));
Исключение
// Пропускаем некорректные символы
КонецПопытки;
КонецЕсли;
КонецЦикла;
// Если массив не отсортирован, сортируем значения через список значений
Список = Новый СписокЗначений;
Список.ЗагрузитьЗначения(МассивЧисел);
Список.СортироватьПоЗначению();
МассивЧисел = Список.ВыгрузитьЗначения();
Если МассивЧисел.Количество() = 0 Тогда
Возврат ТЗ;
КонецЕсли;
НачалоИнтервала = МассивЧисел[0];
ПредыдущееЧисло = МассивЧисел[0];
ТекущийИнтервалКоды = Новый Массив;
ТекущийИнтервалКоды.Добавить(НачалоИнтервала);
Для Индекс = 1 По МассивЧисел.ВГраница() Цикл
ТекЧисло = МассивЧисел[Индекс];
Если ТекЧисло = ПредыдущееЧисло + 1 Тогда
ТекущийИнтервалКоды.Добавить(ТекЧисло);
Иначе
// Закрываем предыдущий интервал
ЗаписатьИнтервалВТаблицу(ТЗ, ТекущийИнтервалКоды, НачалоИнтервала, ПредыдущееЧисло);
// Начинаем новый интервал
ТекущийИнтервалКоды.Очистить();
НачалоИнтервала = ТекЧисло;
ТекущийИнтервалКоды.Добавить(ТекЧисло);
КонецЕсли;
ПредыдущееЧисло = ТекЧисло;
КонецЦикла;
// Записываем последний интервал
ЗаписатьИнтервалВТаблицу(ТЗ, ТекущийИнтервалКоды, НачалоИнтервала, ПредыдущееЧисло);
Возврат ТЗ;
КонецФункции
Процедура ЗаписатьИнтервалВТаблицу(ТЗ, МассивКодов, НачалоИнтервала, КонецИнтервала)
Если НачалоИнтервала = КонецИнтервала Тогда
Представление = Формат(НачалоИнтервала, "ЧГ=0");
Иначе
Представление = Формат(НачалоИнтервала, "ЧГ=0") + " - " + Формат(КонецИнтервала, "ЧГ=0");
КонецЕсли;
Для Каждого Код Из МассивКодов Цикл
НоваяСтрока = ТЗ.Добавить();
НоваяСтрока.Код = Код;
НоваяСтрока.ИмяИнтервала = Представление;
КонецЦикла;
КонецПроцедуры
ЧГ=0 (Формат(Код, "ЧГ=0")). В противном случае платформа автоматически добавит неразрывные пробелы в разряды чисел (например, 1 000 вместо 1000).ВЫБРАТЬ ... ОБЪЕДИНИТЬ ВСЕ. Это создает избыточную нагрузку на компилятор запросов СУБД и может вызвать ошибку переполнения стека парсера SQL. Передача готовой таблицы значений через параметры запроса или внешний набор данных работает стабильнее и быстрее.