При разработке различных отчетов, обработок и механизмов интеграции в среде 1С программисты часто сталкиваются с необходимостью агрегации данных в коллекциях. Типичный пример: у нас есть Таблица Значений (ТЗ), содержащая историю оплат контрагентов. В ней присутствует множество строк по каждому клиенту с разными датами и суммами. Наша задача — свернуть эту таблицу так, чтобы по каждому контрагенту осталась ровно одна строка, в которой будет находиться общая сумма всех его оплат, а в поле даты будет записана дата самого последнего (максимального) платежа.
Давайте подробно разберем, почему стандартные механизмы группировки здесь не всегда работают напрямую, и проанализируем пять различных способов решения этой задачи: от простого перебора до высокопроизводительных алгоритмов с использованием индексов, таблиц соответствий и механизмов запросов.
Первое, что приходит на ум любому разработчику 1С при необходимости сгруппировать таблицу значений — это стандартный метод Свернуть(). Однако этот метод имеет жесткие ограничения. Он умеет разделять колонки только на два типа: группировочные (по которым строки объединяются) и суммируемые (числовые ресурсы, значения которых складываются).
Если мы попытаемся применить Свернуть() к нашей задаче, мы столкнемся с проблемой обработки даты:
Именно поэтому нам необходимо использовать альтернативные программные алгоритмы решения.
Рассмотрим базовый алгоритм решения, предложенный на форуме. Суть метода заключается в предварительном получении списка уникальных контрагентов, последующем поиске всех строк по каждому из них и ручном вычислении суммы и максимальной даты.
Разберем этот алгоритм по шагам:
Свернуть().НайтиСтроки().Посмотрим на практический пример реализации этого метода:
&НаСервереБезКонтекста
Функция ОбработатьТаблицуОплат(ИсходнаяТЗ)
// Шаг 1. Создаем структуру результирующей таблицы
Результат = ИсходнаяТЗ.СкопироватьКолонки("Контрагент, ДатаОплаты, Сумма");
// Шаг 2. Получаем только уникальных контрагентов
ВременнаяТаб = ИсходнаяТЗ.Скопировать(, "Контрагент");
ВременнаяТаб.Свернуть("Контрагент", "");
Контрагенты = ВременнаяТаб.ВыгрузитьКолонку("Контрагент");
// Шаг 3. Обходим уникальных контрагентов в цикле
Для Каждого ТекущийКонтрагент Из Контрагенты Цикл
МаксДатаОплаты = Дата(1, 1, 1);
ОбщаяСумма = 0;
// Шаг 4. Находим все строки по текущему контрагенту
ПараметрыОтбора = Новый Структура("Контрагент", ТекущийКонтрагент);
НайденныеСтроки = ИсходнаяТЗ.НайтиСтроки(ПараметрыОтбора);
// Шаг 5. Вычисляем сумму и ищем максимальную дату
Для Каждого СтрокаТаб Из НайденныеСтроки Цикл
ОбщаяСумма = ОбщаяСумма + СтрокаTab.Сумма;
Если СтрокаТаб.ДатаОплаты > МаксДатаОплаты Тогда
МаксДатаОплаты = СтрокаТаб.ДатаОплаты;
КонецЕсли;
КонецЦикла;
// Шаг 6. Заполняем итоговую таблицу
НоваяСтрока = Результат.Добавить();
НоваяСтрока.Контрагент = ТекущийКонтрагент;
НоваяСтрока.ДатаОплаты = МаксДатаОплаты;
НоваяСтрока.Сумма = ОбщаяСумма;
КонецЦикла;
Возврат Результат;
КонецФункции
Этот метод прост для понимания и отлично работает на небольших таблицах значений (до нескольких сотен строк). Однако у него есть существенный недостаток: производительность. Метод НайтиСтроки() выполняет последовательный обход таблицы при каждом вызове. Если у вас в таблице 10 000 строк и 1 000 уникальных контрагентов, система выполнит огромное количество внутренних итераций, что приведет к заметному замедлению работы.
Если мы хотим оставить логику первого способа, но значительно ускорить ее работу на больших объемах данных, нам обязательно нужно использовать индексирование. Добавление индекса по колонке поиска позволяет платформе 1С мгновенно находить нужные строки без полного сканирования таблицы значений.
Давайте проанализируем, как изменится код при добавлении индекса:
&НаСервереБезКонтекста
Функция ОбработатьТаблицуСИндексом(ИсходнаяТЗ)
Результат = ИсходнаяТЗ.СкопироватьКолонки("Контрагент, ДатаОплаты, Сумма");
// Добавляем индекс для ускорения работы метода НайтиСтроки()
ИсходнаяТЗ.Индексы.Добавить("Контрагент");
ВременнаяТаб = ИсходнаяТЗ.Скопировать(, "Контрагент");
ВременнаяТаб.Свернуть("Контрагент", "");
Для Каждого СтрокаУник Из ВременнаяТаб Цикл
МаксДатаОплаты = Дата(1, 1, 1);
ОбщаяСумма = 0;
ПараметрыОтбора = Новый Структура("Контрагент", СтрокаУник.Контрагент);
// Благодаря индексу этот поиск выполнится мгновенно
НайденныеСтроки = ИсходнаяТЗ.НайтиСтроки(ПараметрыОтбора);
Для Каждого СтрокаТаб Из НайденныеСтроки Цикл
ОбщаяСумма = ОбщаяСумма + СтрокаТаб.Сумма;
Если СтрокаТаб.ДатаОплаты > МаксДатаОплаты Тогда
МаксДатаОплаты = СтрокаТаб.ДатаОплаты;
КонецЕсли;
КонецЦикла;
НоваяСтрока = Результат.Добавить();
НоваяСтрока.Контрагент = СтрокаУник.Контрагент;
НоваяСтрока.ДатаОплаты = МаксДатаОплаты;
НоваяСтрока.Сумма = ОбщаяСумма;
КонецЦикла;
Возврат Результат;
КонецФункции
Использование свойства Индексы — это самый простой способ ускорить поисковые операции в таблицах значений в разы, не переписывая при этом основной каркас алгоритма.
Рассмотрим более элегантный алгоритмический подход. Вместо того чтобы сначала собирать уникальных контрагентов, а затем искать строки по каждому из них, мы можем обойти исходную таблицу значений всего один раз (в один цикл).
Для этого мы применим объект Соответствие, где ключом будет выступать объект контрагента, а значением — структура, содержащая текущую накопленную сумму и максимальную найденную дату. Этот подход работает по принципу кэширования "на лету".
Разберем по шагам реализацию этого быстрого алгоритма:
&НаСервереБезКонтекста
Функция ОбработатьЧерезСоответствие(ИсходнаяТЗ)
// Кэш для хранения промежуточных результатов расчета
ТаблицаСоответствий = Новый Соответствие;
// Проходим по таблице значений ровно один раз
Для Каждого СтрокаТЗ Из ИсходнаяТЗ Цикл
ДанныеКонтрагента = ТаблицаСоответствий.Получить(СтрокаТЗ.Контрагент);
Если ДанныеКонтрагента = Неопределено Тогда
// Если контрагента еще нет в кэше, инициализируем структуру
ДанныеКонтрагента = Новый Структура("Сумма, ДатаОплаты", СтрокаТЗ.Сумма, СтрокаТЗ.ДатаОплаты);
ТаблицаСоответствий.Вставить(СтрокаТЗ.Контрагент, ДанныеКонтрагента);
Иначе
// Если контрагент уже встречался, обновляем его показатели
ДанныеКонтрагента.Сумма = ДанныеКонтрагента.Сумма + СтрокаТЗ.Сумма;
Если СтрокаТЗ.ДатаОплаты > ДанныеКонтрагента.ДатаОплаты Тогда
ДанныеКонтрагента.ДатаОплаты = СтрокаТЗ.ДатаОплаты;
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Заполняем результирующую таблицу значений на основе накопленного кэша
Результат = ИсходнаяТЗ.СкопироватьКолонки("Контрагент, ДатаОплаты, Сумма");
Для Каждого ЭлементКэша Из ТаблицаСоответствий Цикл
НоваяСтрока = Результат.Добавить();
НоваяСтрока.Контрагент = ЭлементКэша.Ключ;
НоваяСтрока.Сумма = ЭлементКэша.Значение.Сумма;
НоваяСтрока.ДатаОплаты = ЭлементКэша.Значение.ДатаОплаты;
КонецЦикла;
Возврат Результат;
КонецФункции
Преимущество метода: Высочайшая скорость работы на уровне встроенного языка 1С. Алгоритм имеет линейную сложность O(N), так как мы избавлены от вложенных циклов и повторных сканирований таблицы.
Еще один классический программистский прием — предварительное упорядочивание данных. Если мы отсортируем таблицу значений сначала по контрагенту по возрастанию, а затем по дате оплаты по убыванию, то самая поздняя (максимальная) дата для каждого контрагента гарантированно окажется в самой первой строчке этого контрагента.
Давайте посмотрим, как использовать эту особенность сортировки для решения нашей задачи:
&НаСервереБезКонтекста
Функция ОбработатьЧерезСортировку(ИсходнаяТЗ)
Результат = ИсходнаяТЗ.СкопироватьКолонки("Контрагент, ДатаОплаты, Сумма");
// Копируем исходную таблицу, чтобы не нарушать порядок в оригинале
РабочаяТЗ = ИсходнаяТЗ.Скопировать();
// Сортируем: контрагенты по алфавиту, а даты внутри контрагента - от новых к старым
РабочаяТЗ.Сортировать("Контрагент Возр, ДатаОплаты Убыв");
ТекущийКонтрагент = Неопределено;
НакопленнаяСумма = 0;
ПоследняяДата = Дата(1, 1, 1);
Для Каждого СтрокаТЗ Из РабочаяТЗ Цикл
Если СтрокаТЗ.Контрагент <> ТекущийКонтрагент Тогда
// Если сменился контрагент, записываем данные предыдущего (если он был)
Если ТекущийКонтрагент <> Неопределено Тогда
НоваяСтрока = Результат.Добавить();
НоваяСтрока.Контрагент = ТекущийКонтрагент;
НоваяСтрока.Сумма = НакопленнаяСумма;
НоваяСтрока.ДатаОплаты = ПоследняяДата;
КонецЕсли;
// Инициализируем показатели для нового контрагента
ТекущийКонтрагент = СтрокаТЗ.Контрагент;
НакопленнаяСумма = СтрокаТЗ.Сумма;
// Так как сортировка по убыванию даты, первая дата - всегда максимальная!
ПоследняяДата = СтрокаТЗ.ДатаОплаты;
Иначе
// Для того же контрагента просто накапливаем сумму, дату игнорируем
НакопленнаяСумма = НакопленнаяСумма + СтрокаТЗ.Сумма;
КонецЕсли;
КонецЦикла;
// Не забываем записать данные по самому последнему контрагенту после окончания цикла
Если ТекущийКонтрагент <> Неопределено Тогда
НоваяСтрока = Результат.Добавить();
НоваяСтрока.Контрагент = ТекущийКонтрагент;
НоваяСтрока.Сумма = НакопленнаяСумма;
НоваяСтрока.ДатаОплаты = ПоследняяДата;
КонецЕсли;
Возврат Результат;
КонецФункции
Метод сортировки также работает очень быстро, поскольку встроенные механизмы сортировки таблиц значений в 1С оптимизированы на уровне платформы и выполняются на языке C++.
Несмотря на то, что автор темы просил решить задачу именно "кодом", в профессиональной среде разработки 1С наиболее лаконичным, надежным и стандартным решением является использование механизма запросов. Таблицу значений можно передать в запрос в качестве параметра, предварительно описав ее типы.
Запрос позволяет автоматически выполнить всю группировку, используя мощные встроенные агрегатные функции СУБД (или встроенной СУБД для временных таблиц): СУММА() для суммирования оплат и МАКСИМУМ() для вычисления последней даты оплаты.
Давайте разберем по шагам этот красивый и правильный метод:
&НаСервереБезКонтекста
Функция ОбработатьЧерезЗапрос(ИсходнаяТЗ)
// Запросу необходимо четко знать типы колонок таблицы значений.
// Если они не заданы при создании ТЗ, рекомендуется их типизировать явным образом.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// Передаем таблицу значений в параметры запроса
Запрос.УстановитьПараметр("ТаблицаОплат", ИсходнаяТЗ);
// Описываем запрос к нашей таблице значений
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.Контрагент КАК Контрагент,
| ТЗ.ДатаОплаты КАК ДатаОплаты,
| ТЗ.Сумма КАК Сумма
|ПОМЕСТИТЬ ВТ_Оплаты
|ИЗ
| &ТаблицаОплат КАК ТЗ
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Оплаты.Контрагент КАК Контрагент,
| МАКСИМУМ(ВТ_Оплаты.ДатаОплаты) КАК ДатаОплаты,
| СУММА(ВТ_Оплаты.Сумма) КАК Сумма
|ИЗ
| ВТ_Оплаты КАК ВТ_Оплаты
|
|СГРУППИРОВАТЬ ПО
| ВТ_Оплаты.Контрагент";
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Возврат РезультатЗапроса.Выгрузить();
Иначе
// Возвращаем пустую копию структуры исходной таблицы
Возврат ИсходнаяТЗ.СкопироватьКолонки("Контрагент, ДатаОплаты, Сумма");
КонецЕсли;
КонецФункции
Преимущества работы через запрос:
Мы детально проанализировали пять подходов к группировке таблицы значений в 1С с одновременным суммированием числовых данных и поиском максимальной даты. Если вы пишете простой код для небольших локальных таблиц, отлично подойдет Способ 3 (через Соответствие) или Способ 4 (сортировка). Если вы стремитесь к соответствию стандартам разработки фирмы "1С" и хотите обеспечить максимальную скорость работы — всегда отдавайте предпочтение Способу 5 (через Запросы).