При разработке отчетов и обработок в среде 1С:Предприятие часто возникает задача вывода сгруппированных данных. Например, когда таблица значений содержит список строк по различным документам (документ1, документ1, документ2, документ2, документ2), и нам требуется определить последнюю строку каждой группы для того, чтобы вывести промежуточный итог, заполнить специальную колонку (например, информацию о бонусной накладной) или применить особое форматирование.
В этой статье мы подробно разберем несколько способов решения данной задачи. Мы изучим как простые варианты обхода таблицы значений с анализом индексов, так и более продвинутые архитектурные решения, включая буферизацию данных, использование механизмов запросов с итогами и Систему компоновки данных (СКД). Мы также проанализируем скрытые ловушки производительности, которые могут замедлить работу вашей программы в десятки раз.
Рассмотрим пошагово каждый из подходов, их достоинства, недостатки и примеры реализации.
---
Самый очевидный способ определить, является ли текущая строка последней в группе одинаковых документов — это заглянуть на одну строку вперед. Если в следующей строке таблицы содержится уже другой документ (или если следующей строки просто нет, так как текущая строка — последняя в таблице), значит, мы нашли границу группы.
Для реализации этого алгоритма нам понадобится знать индекс текущей строки. Выясним причину, почему стандартный обход через Для Каждого здесь может быть неудобен, и посмотрим на пример правильной реализации с использованием цикла по индексу.
Проанализируем следующий пример кода:
КоличествоСтрок = ТЗ.Количество();
Для Индекс = 0 По КоличествоСтрок - 1 Цикл
ТекущаяСтрока = ТЗ[Индекс];
ТекущийДокумент = ТекущаяСтрока.РегистраторДокументПродажи;
// Выполняем заполнение основных данных строки табличного документа
// ... (код заполнения параметров области)
// Проверяем, является ли текущая строка последней в группе документов
Если (Индекс = КоличествоСтрок - 1) ИЛИ (ТекущийДокумент <> ТЗ[Индекс + 1].РегистраторДокументПродажи) Тогда
// Это последняя строка в группе документов!
// Заполняем колонку с итогом или бонусом
ТекущаяСтрока.БонуснаяНакладная = "Итог по документу";
// Если рассчитываем показатели на лету:
// ОбластьСтрока.Параметры.РОИ = РассчитатьРОИ(СуммаЗаказа, ПредоставленоСкидки);
КонецЕсли;
// Выводим строку в табличный документ
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
Важный нюанс: механизм ленивых вычислений (Short-circuit evaluation). Обратите внимание на условие проверки:
Если (Индекс = КоличествоСтрок - 1) ИЛИ (ТекущийДокумент <> ТЗ[Индекс + 1].РегистраторДокументПродажи) Тогда
Платформа 1С использует ленивое вычисление логических выражений. Если первая часть условия перед оператором ИЛИ возвращает Истина (то есть мы находимся на самой последней строке таблицы значений), то вычисление второй части выражения даже не начинается. Благодаря этому не происходит обращения к несуществующему индексу Индекс + 1, и программа не падает с ошибкой "Индекс находится за границами массива". Если вы поменяете эти условия местами, программа гарантированно завершится с ошибкой при обработке последней строки.
Критическая ловушка производительности! Избегайте использования метода ТЗ.Индекс(Строка) внутри цикла Для Каждого. Посмотрим на типичную ошибку начинающих разработчиков:
// ТАК ДЕЛАТЬ НЕЛЬЗЯ! Квадратичная сложность O(N^2)
Для Каждого СтрокаТЗ Из ТЗ Цикл
ТекущийИндекс = ТЗ.Индекс(СтрокаТЗ); // Метод перебирает ТЗ с начала для поиска индекса!
Если (ТекущийИндекс = ТЗ.Количество() - 1) ИЛИ (СтрокаТЗ.Документ <> ТЗ[ТекущийИндекс + 1].Документ) Тогда
// Код обработки
КонецЕсли;
КонецЦикла;
Метод Индекс() у таблицы значений не хранит позицию строки в памяти как константу. Чтобы найти индекс, платформа вынуждена каждый раз выполнять линейный поиск, перебирая строки таблицы с самого начала. При обходе таблицы из 10 000 строк этот цикл выполнит порядка 50 миллионов операций сравнения, что приведет к полному зависанию системы. Используйте только обход по целочисленному счетчику (цикл Для... По...).
---
Чтобы полностью исключить риск возникновения ошибок с выходом индекса за границы массива и сделать код максимально понятным и быстрым (со сложностью алгоритма $O(N)$), применим метод сравнения с предыдущим элементом. Мы будем двигаться по циклу Для Каждого, накапливать данные и фиксировать итоги "задним числом" в момент, когда обнаруживаем смену документа.
Разберем этот алгоритм по шагам:
Неопределено).Посмотрим на практическую реализацию этого метода:
ПредыдущийДокумент = Неопределено;
СтрокаДляИтога = Неопределено;
// Накапливаемые показатели по группе документов
СуммаГруппы = 0;
СкидкаГруппы = 0;
Для Каждого СтрокаТЗ Из ТЗ Цикл
ТекущийДокумент = СтрокаТЗ.РегистраторДокументПродажи;
// Проверяем, изменился ли документ по сравнению с предыдущей строкой
Если ПредыдущийДокумент <> Неопределено И ПредыдущийДокумент <> ТекущийДокумент Тогда
// Мы перешли на новый документ. СтрокаДляИтога гарантированно является последней в прошлой группе!
// Записываем накопленные показатели в буферную строку
СтрокаДляИтога.БонуснаяНакладная = "Итог: " + СуммаГруппы;
// Здесь же мы можем вывести оформленную область в табличный документ, если это отчет
// ТабДок.Вывести(ОбластьСтрока);
// Сбрасываем накопители для новой группы документов
СуммаГруппы = 0;
СкидкаГруппы = 0;
КонецЕсли;
// Накапливаем показатели текущей строки
СуммаГруппы = СуммаГруппы + СтрокаТЗ.Сумма;
СкидкаГруппы = СкидкаГруппы + СтрокаТЗ.Скидка;
// Запоминаем текущую строку как потенциально последнюю строку группы
ПредыдущийДокумент = ТекущийДокумент;
СтрокаДляИтога = СтрокаТЗ;
// Заполняем стандартные реквизиты строки (без итогов)
// ...
КонецЦикла;
// ВАЖНО: обрабатываем самую последнюю строку всей таблицы значений
Если СтрокаДляИтога <> Неопределено Тогда
СтрокаДляИтога.БонуснаяНакладная = "Итог: " + СуммаГруппы;
// ТабДок.Вывести(ОбластьСтрока);
КонецЕсли;
Этот способ работает невероятно быстро, не использует индексные операции, легко читается и полностью защищен от ошибок выхода за пределы допустимых значений индексов.
---
Если данные для вашей таблицы значений изначально выбираются из базы данных с помощью запроса, то рассчитывать итоги и группировать данные вручную в коде 1С — не лучшая практика. Гораздо эффективнее переложить эту задачу на СУБД. В платформе 1С для этого существует мощный инструмент — предложение ИТОГИ в тексте запроса.
Рассмотрим подробнее, как переписать алгоритм с использованием итогов запроса. Это позволит нам полностью отказаться от вычисления "последней строки группы" вручную.
Сначала пишем текст запроса, где указываем группировку и суммируемые поля:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РеализацияТоваровУслуг.Ссылка КАК РегистраторДокументПродажи,
| РеализацияТоваровУслуг.Контрагент КАК Контрагент,
| РеализацияТоваровУслуг.СуммаДокумента КАК Сумма,
| РеализацияТоваровУслуг.Номенклатура КАК Номенклатура,
| РеализацияТоваровУслуг.Количество КАК Количество
|ИЗ
| Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
|ГДЕ
| РеализацияТоваровУслуг.Дата МЕЖДУ &НачДата И &КонДата
|
|УПОРЯДОЧИТЬ ПО
| РегистраторДокументПродажи
|ИТОГИ
| СУММА(Сумма),
| СУММА(Количество)
|ПО
| РегистраторДокументПродажи";
Запрос.УстановитьПараметр("НачДата", НачДата);
Запрос.УстановитьПараметр("КонДата", КонДата);
РезультатЗапроса = Запрос.Выполнить();
Теперь мы можем использовать двухуровневый обход выборки: первый уровень — по группировкам (документам), второй уровень — по детальным записям (строкам товаров внутри документа). Это избавляет нас от написания сложных условий в коде.
// Обходим группировку первого уровня (Документы)
ВыборкаДокумент = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДокумент.Следующий() Цикл
// Получаем детальные записи по текущему документу
ВыборкаДетальные = ВыборкаДокумент.Выбрать();
КоличествоДетальных = ВыборкаДетальные.Количество();
СчетчикДетальных = 0;
Пока ВыборкаДетальные.Следующий() Цикл
СчетчикДетальных = СчетчикДетальных + 1;
// Заполняем стандартную строку табличного документа
ОбластьСтрока.Параметры.Номенклатура = ВыборкаДетальные.Номенклатура;
ОбластьСтрока.Параметры.Количество = ВыборкаДетальные.Количество;
ОбластьСтрока.Параметры.Сумма = ВыборкаДетальные.Сумма;
// Проверяем, является ли текущая детальная строка последней в документе
Если СчетчикДетальных = КоличествоДетальных Тогда
// Это последняя строка группы документов! Выводим итог именно сюда
ОбластьСтрока.Параметры.БонуснаяНакладная = "Итог по документу: " + ВыборкаДокумент.Сумма;
Иначе
// Очищаем параметр для остальных строк, чтобы данные не дублировались
ОбластьСтрока.Параметры.БонуснаяНакладная = "";
КонецЕсли;
ТабДок.Вывести(ОбластьСтрока);
КонецЦикла;
КонецЦикла;
Преимущества работы через выборку запроса:
ВыборкаДокумент.---
Если вашей конечной целью является вывод отчета на экран пользователя, то самым современным и правильным решением будет полный отказ от ручного вывода строк через макеты табличного документа в пользу Системы компоновки данных (СКД). СКД — это декларативный механизм платформы 1С, который берет на себя все рутинные задачи группировки, сортировки и вывода итогов.
Для реализации нашей задачи в СКД достаточно выполнить следующие простые настройки:
ИТОГИ, просто плоскую таблицу).Сумма и Количество. В качестве выражения указать функцию Сумма(...). Это позволит автоматически рассчитывать итоги по группам.РегистраторДокументПродажи. Внутри этой группировки добавить вложенную группировку Детальные записи.Использование СКД позволяет сократить объем программного кода практически до нуля, полностью исключает программные ошибки ручного формирования отчетов и дает пользователю мощный инструмент для кастомизации отчета (группировки, отборы, условное оформление).
---
Давайте сопоставим все рассмотренные варианты, чтобы вы могли легко выбрать наиболее подходящий инструмент для решения вашей текущей задачи:
| Метод | Сложность алгоритма | Безопасность (риск ошибок) | Рекомендуемая область применения |
|---|---|---|---|
| 1. Сравнение со следующей строкой ТЗ[Индекс + 1] | $O(N)$ (при использовании правильного цикла по индексу) | Средняя (требуется строгий контроль ленивых вычислений и последней строки) | Быстрые локальные доработки существующего legacy-кода, где уже используется обход ТЗ. |
| 2. Буферизация и сравнение "назад" | $O(N)$ (один линейный проход) | Высокая (нет индексных операций, ошибки выхода за границы массива исключены) | Универсальный алгоритм для обработки любых коллекций данных в памяти (ТЗ, Дерево значений, Массивы). |
| 3. Итоги в запросе (двухуровневая выборка) | $O(N)$ на стороне 1С, СУБД оптимизирует группировку | Максимальная (структура выборки гарантирует правильное разделение на группы) | Создание новых сложных печатных форм и отчетов, формируемых вручную через код. |
| 4. Разработка отчета на СКД | Оптимизировано платформой | Абсолютная безопасность (код не пишется вручную) | Разработка любых пользовательских отчетов с группировками и итогами. Стандарт современной разработки в 1С. |
Выбирайте метод исходя из контекста вашей задачи. Если вы поддерживаете старый отчёт с ручным выводом областей — используйте однопроходный обход с буферизацией (Способ №2). Если вы пишете функционал с нуля — всегда отдавайте предпочтение запросам с итогами (Способ №3) или полноценной разработке отчета на СКД (Способ №4).