В процессе разработки часто возникает задача поиска экстремальных значений (максимума или минимума) в таблице значений. Классический пример — получение даты последнего платежа для каждого контрагента из общего списка оплат. В этой статье мы подробно разберем несколько способов решения этой задачи: от простого перебора коллекций до использования механизмов запросов и построителя отчетов.
Проанализируем ситуацию, когда нам нужно найти максимальную дату для каждого уникального ФИО или Контрагента. Использование объекта Соответствие является наиболее универсальным программным методом, так как он позволяет использовать в качестве ключа строки с пробелами (например, "Иванов Иван Иванович") или ссылки на справочники.
Рассмотрим пошагово алгоритм реализации через цикл:
// Создаем соответствие для хранения пар "Контрагент - МаксимальнаяДата"
ТаблицаРезультатов = Новый Соответствие;
Для Каждого СтрокаТЗ Из ТаблицаОплат Цикл
// Пытаемся получить текущую дату для данного контрагента из соответствия
ТекущаяМаксДата = ТаблицаРезультатов.Получить(СтрокаТЗ.Контрагент);
Если ТекущаяМаксДата = Неопределено Тогда
// Если контрагента еще нет в списке, просто добавляем его дату
ТаблицаРезультатов.Вставить(СтрокаТЗ.Контрагент, СтрокаТЗ.Дата);
Иначе
// Если контрагент уже есть, сравниваем даты и записываем наибольшую
МаксимальнаяДата = Макс(СтрокаТЗ.Дата, ТекущаяМаксДата);
ТаблицаРезультатов.Вставить(СтрокаТЗ.Контрагент, МаксимальнаяДата);
КонецЕсли;
КонецЦикла;
Этот метод хорош тем, что он работает быстро и не накладывает ограничений на содержимое колонки Контрагент. В результате мы получаем коллекцию, которую легко обойти и использовать для дальнейших расчетов.
Объект Структура работает аналогично соответствию, но имеет важное техническое ограничение: ключ структуры должен соответствовать правилам именования переменных в 1С. Это значит, что в имени ключа не должно быть пробелов, спецсимволов, и оно не может начинаться с цифры.
Выясним причину, почему это может быть неудобно: если ваш контрагент записан как "ООО Ромашка", код выдаст ошибку при попытке обращения к свойству. Однако, если вы используете идентификаторы без пробелов, код будет выглядеть так:
Данные = Новый Структура;
Для Каждого Стр Из ТЗ Цикл
Если Данные.Свойство(Стр.Имя) = Неопределено Тогда
Данные.Вставить(Стр.Имя, Стр.Дата);
Иначе
Данные.Вставить(Стр.Имя, Макс(Данные[Стр.Имя], Стр.Дата));
КонецЕсли;
КонецЦикла;
Важно: Если вы столкнулись с ошибкой при использовании метода Свойство, проверьте содержимое переменной, передаваемой в качестве ключа. Если там есть пробелы — переходите к использованию Соответствие.
Разберем еще один изящный способ, который позволяет обойтись без создания дополнительных коллекций типа структур. Мы можем отсортировать таблицу таким образом, чтобы нужные нам данные оказались вверху.
ТЗ.Сортировать("Дата Убыв");Найти для поиска первой попавшейся строки по нужному контрагенту.Поскольку таблица отсортирована "от большего к меньшему", метод Найти вернет именно ту строку, где дата является максимальной. Посмотрим на пример кода:
ТЗ.Сортировать("Дата Убыв");
ПараметрыОтбора = Новый Структура("Контрагент", "Иванов");
// Метод Найти вернет первую подходящую строку, а она у нас самая свежая
СтрокаСМаксДатой = ТЗ.Найти("Иванов", "Контрагент");
Если СтрокаСМаксДатой <> Неопределено Тогда
Сообщить("Последняя оплата: " + СтрокаСМаксДатой.Дата);
КонецЕсли;
Профессиональным подходом считается использование механизма запросов. Это особенно актуально, если таблица значений содержит тысячи строк. Запрос выполнит группировку и поиск максимума гораздо быстрее, чем ручной перебор в цикле Для Каждого.
Рассмотрим подробнее реализацию этого метода. Для того чтобы передать таблицу в запрос, необходимо выполнить два условия:
ОписаниеТипов при создании ТаблицаЗначений).МенеджерВременныхТаблиц.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.УстановитьПараметр("ВнешняяТЗ", МояТаблицаОплат);
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.Контрагент КАК Контрагент,
| ТЗ.Дата КАК Дата
|ПОМЕСТИТЬ ВТ_Данные
|ИЗ
| &ВнешняяТЗ КАК ТЗ
|;
|
|ВЫБРАТЬ
| ВТ_Данные.Контрагент КАК Контрагент,
| МАКСИМУМ(ВТ_Данные.Дата) КАК МаксДата
|ИЗ
| ВТ_Данные КАК ВТ_Данные
|СГРУППИРОВАТЬ ПО
| Контрагент";
Результат = Запрос.Выполнить().Выгрузить();
Этот метод является наиболее масштабируемым и "чистым" с точки зрения архитектуры 1С.
Если вы работаете в старых конфигурациях или на обычных формах, полезно знать про ПостроительЗапроса. Он позволяет выполнить агрегатные функции над таблицей значений без явного написания текста запроса к временным таблицам.
ИстДанных = Новый ОписаниеИсточникаДанных(ТЗ);
// Указываем, что по колонке Дата нужно посчитать максимум
ИстДанных.Колонки.Дата.Итог = "Максимум(Дата)";
Построитель = Новый ПостроительЗапроса;
Построитель.ИсточникДанных = ИстДанных;
Построитель.Выполнить();
// В результате получим сводную таблицу
РезультатТЗ = Построитель.Результат.Выгрузить();
Проанализируем несколько важных моментов, которые могут повлиять на корректность результата:
Индексация: Если вы решили использовать метод НайтиСтроки или Найти в цикле, обязательно добавьте индекс по колонке поиска: ТЗ.Индексы.Добавить("Контрагент");. Это ускорит работу в десятки раз на больших объемах данных.
Пустые даты: При сравнении дат через функцию Макс(), помните, что пустая дата '00010101' всегда будет меньше любой заполненной даты. Однако, если вам нужно найти минимальную дату, пустые значения придется обрабатывать отдельно, чтобы они не становились результатом поиска.
Типизация: При создании таблицы значений всегда указывайте типы колонок. Это не только позволит использовать запросы, но и защитит от ошибок сравнения, когда в колонке внезапно оказывается Неопределено или значение другого типа.
Таким образом, мы разобрали основные методики поиска максимальной даты. Для простых задач и небольших таблиц идеально подойдет Соответствие или Сортировка, а для сложных отчетов и больших массивов данных лучше использовать Запросы.