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