Порядок строк в ДеревоЗначений: от одной колонки до правил по уровням
Содержание
Дерево значений в 1С сортируют методом Сортировать() у коллекции строк: Дерево.Строки.Сортировать("Наименование", Истина). Первый параметр — список колонок с направлением, второй — Истина, если нужно отсортировать и все вложенные уровни. Без второго параметра упорядочится только верхний уровень.
Этого хватает в большинстве простых задач. Сложности начинаются, когда группы надо выстроить по сумме, уровни — по разным правилам, а колонка хранит ссылки или числа в виде строк. Каждый такой случай ниже разобран с рабочим кодом.
Метод Сортировать у строк дерева: как он устроен
У дерева нет одной плоской таблицы. Каждая строка хранит свою коллекцию подчинённых строк — свойство Строки. Поэтому метод сортировки вызывают не у самого дерева, а у коллекции строк нужного уровня.
Параметры метода:
| Параметр | Что передать | По умолчанию |
|---|---|---|
| Колонки | Строка: имена колонок через запятую, после имени — Возр или Убыв | — (обязателен) |
| Сортировать вложенные | Истина — упорядочить все уровни ниже | Ложь |
| Объект сравнения | Новый СравнениеЗначений | не задан |
Соберём небольшое дерево и отсортируем его:
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"));
Дерево.Колонки.Добавить("Дата", Новый ОписаниеТипов("Дата"));
Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Склад Б";
Строка = Группа.Строки.Добавить();
Строка.Наименование = "Цемент";
Строка.Сумма = 90000;
Строка = Группа.Строки.Добавить();
Строка.Наименование = "Арматура";
Строка.Сумма = 80000;
Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Склад А";
Строка = Группа.Строки.Добавить();
Строка.Наименование = "Кирпич";
Строка.Сумма = 120000;
Строка = Группа.Строки.Добавить();
Строка.Наименование = "Песок";
Строка.Сумма = 30000;
// Склады по алфавиту, товары внутри складов тоже по алфавиту
Дерево.Строки.Сортировать("Наименование Возр", Истина);
Результат: «Склад А» (Кирпич, Песок), затем «Склад Б» (Арматура, Цемент).
Ключевые моменты:
- 🌳 Иерархия сохраняется. Строка никогда не переезжает к чужому родителю — сортируются только «братья» внутри одной группы.
- 🔁 С
Истинаметод сам обходит все уровни. Писать рекурсию для одинакового правила не нужно. - 🎯 Можно сортировать одну ветку:
Группа.Строки.Сортировать("Сумма Убыв")— остальное дерево не тронется.
Направление Возр можно не писать — оно применяется по умолчанию. Но в коде с несколькими колонками лучше указывать явно: так правило читается без справки.
Сортировка дерева значений по нескольким колонкам
Сложное правило задаётся одной строкой. Колонки перечисляют через запятую в порядке приоритета. Вторая колонка работает только там, где значения первой совпали.
// Сначала свежие даты, при равной дате — большие суммы
Дерево.Строки.Сортировать("Дата Убыв, Сумма Убыв", Истина);
Строку с правилом удобно собирать из настроек, а не держать в коде. Например, пользователь выбрал колонку и направление в форме отчёта:
Функция ПравилоСортировки(ИмяКолонки, ПоУбыванию)
Направление = ?(ПоУбыванию, "Убыв", "Возр");
// Последняя колонка — Наименование: порядок при равных значениях всегда одинаковый
Возврат СтрШаблон("%1 %2, Наименование Возр", ИмяКолонки, Направление);
КонецФункции
Дерево.Строки.Сортировать(ПравилоСортировки("Сумма", Истина), Истина);
Последняя колонка в правиле — «разрешитель ничьих». Без неё строки с одинаковой суммой могут встать в произвольном порядке, и отчёт будет выглядеть по-разному от запуска к запуску.
Свой порядок, которого нет в данных
Бывает, что нужный порядок не алфавитный и не числовой: группа «Прочее» всегда в конце, строка «Итого» — внизу. Его не вычислить из имени. Решение — служебная колонка с номером:
Дерево.Колонки.Добавить("Порядок", Новый ОписаниеТипов("Число"));
Для Каждого СтрокаДерева Из Дерево.Строки Цикл
Если СтрокаДерева.Наименование = "Прочее" Тогда
СтрокаДерева.Порядок = 1;
Иначе
СтрокаДерева.Порядок = 0;
КонецЕсли;
КонецЦикла;
Дерево.Строки.Сортировать("Порядок Возр, Наименование Возр");
Колонку Порядок потом можно скрыть на форме или не выводить в макет. Пользователь её не видит, а порядок держится.
Рекурсивная сортировка: разные правила для каждого уровня
Параметр Истина применяет одно правило ко всем уровням. А в отчётах часто нужно другое: склады — по алфавиту, документы внутри склада — от новых к старым. Здесь без собственной рекурсии не обойтись.
Процедура СортироватьПоУровням(КоллекцияСтрок, Уровень)
Если Уровень = 0 Тогда
КоллекцияСтрок.Сортировать("Наименование Возр");
Иначе
КоллекцияСтрок.Сортировать("Дата Убыв, Сумма Убыв");
КонецЕсли;
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
Если СтрокаДерева.Строки.Количество() > 0 Тогда
СортироватьПоУровням(СтрокаДерева.Строки, Уровень + 1);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
СортироватьПоУровням(Дерево.Строки, 0);
Как это работает:
- Процедура получает коллекцию одного уровня и сортирует её без второго параметра — только этот уровень.
- Затем обходит строки и для каждой непустой ветки вызывает себя же, увеличив номер уровня.
- Обход идёт по уже отсортированной коллекции, а сортируются вложенные коллекции — поэтому цикл ничего не ломает.
У строки дерева есть и собственный метод Уровень(), он возвращает глубину строки (верхний уровень — 0). Передавать номер параметром быстрее: не нужно спрашивать у каждой строки, а в процедуру сразу приходит готовое число.
Не сортируйте коллекцию, по которой сейчас идёт цикл Для Каждого. Сначала сортировка, потом обход — как в примере выше.
Когда нужна рекурсия, а когда хватит параметра
| Задача | Решение |
|---|---|
| Одно правило на всё дерево | Строки.Сортировать("…", Истина) |
| Только верхний уровень | Строки.Сортировать("…") |
| Одна конкретная ветка | Ветка.Строки.Сортировать("…", Истина) |
| Разные правила по уровням | Своя рекурсивная процедура |
| Отбросить пустые группы и отсортировать | Рекурсия: сначала удалить, потом сортировать |
Сортировка групп по итогам
Частый запрос: показать сверху склад с самой большой выручкой. Проблема в том, что у строки-группы своя колонка Сумма обычно пустая — значения лежат только у подчинённых строк. Сортировка по пустой колонке ничего не даст.
Порядок действий такой: сначала посчитать итоги снизу вверх, потом сортировать.
Функция ЗаполнитьСуммыГрупп(КоллекцияСтрок)
Всего = 0;
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
Если СтрокаДерева.Строки.Количество() > 0 Тогда
СтрокаДерева.Сумма = ЗаполнитьСуммыГрупп(СтрокаДерева.Строки);
КонецЕсли;
Всего = Всего + СтрокаДерева.Сумма;
КонецЦикла;
Возврат Всего;
КонецФункции
ЗаполнитьСуммыГрупп(Дерево.Строки);
Дерево.Строки.Сортировать("Сумма Убыв", Истина);
Проверим на дереве из первого раздела:
- 📦 Склад А: 120 000 + 30 000 = 150 000.
- 📦 Склад Б: 90 000 + 80 000 = 170 000.
После сортировки по убыванию первым идёт «Склад Б» (170 000), внутри него — Цемент (90 000) и Арматура (80 000). Затем «Склад А» (150 000): Кирпич, Песок.
Функция работает для любой глубины: каждая группа получает сумму своих детей, а те, в свою очередь, уже посчитаны рекурсией.
Дерево из запроса с ИТОГИ
Если дерево приходит из запроса, часть работы можно отдать СУБД. Запрос с ИТОГИ ПО при выгрузке ОбходРезультатаЗапроса.ПоГруппировкам даёт дерево, где группы уже содержат суммы. А УПОРЯДОЧИТЬ ПО задаёт порядок записей внутри группировок.
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ; // ВТ_Данные создана ранее
Запрос.Текст =
"ВЫБРАТЬ
| Данные.Склад КАК Склад,
| Данные.Номенклатура КАК Номенклатура,
| СУММА(Данные.Сумма) КАК Сумма
|ИЗ
| ВТ_Данные КАК Данные
|СГРУППИРОВАТЬ ПО
| Данные.Склад,
| Данные.Номенклатура
|УПОРЯДОЧИТЬ ПО
| Данные.Склад.Наименование,
| Сумма УБЫВ
|ИТОГИ
| СУММА(Сумма)
|ПО
| Склад";
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Склады встанут по алфавиту, товары внутри — по убыванию суммы. Если же сами склады нужно выстроить по их итогу, проще после выгрузки вызвать Дерево.Строки.Сортировать("Сумма Убыв") — суммы у групп уже заполнены запросом, функция из примера выше не нужна.
Строки, ссылки и языковые настройки: почему порядок «не тот»
Метод сортирует по значению колонки, а не по тому, что видит пользователь. Отсюда большинство сюрпризов.
Числа, записанные строкой
Колонка типа «Строка» с номерами «1», «2», «10» упорядочится как «1», «10», «2». Сравнение идёт посимвольно, и «10» начинается с единицы.
Два способа исправить:
- Хранить номер в отдельной числовой колонке и сортировать по ней — самый чистый вариант.
- Если колонка обязана быть строкой, дополнить номер нулями слева:
Формат(Номер, "ЧЦ=6; ЧВН=; ЧГ=0")даст «000010». БезЧГ=0в длинные числа попадёт разделитель групп разрядов.
Ссылки на справочники и документы
По колонке со ссылками порядок без третьего параметра не совпадает с алфавитом представлений. Для пользователя это выглядит как случайная перестановка.
- 🔤 Передайте объект сравнения:
Строки.Сортировать("Контрагент", Истина, Новый СравнениеЗначений). Ссылки будут сравниваться по представлению. - ⚡ На больших объёмах надёжнее добавить строковую колонку с представлением и заполнить её сразу в запросе через
ПРЕДСТАВЛЕНИЕ(Данные.Контрагент). Тогда при сортировке не нужно получать представление каждой ссылки.
Смешанные типы в одной колонке
Если колонку создали без ОписаниеТипов, в неё может попасть что угодно: число, строка, Неопределено. Значения разных типов сначала группируются по типу, и строка «Итого» внезапно оказывается среди чисел. Задавайте тип при создании колонки — как в примерах выше.
Буква «ё», регистр и языковые настройки
Строки сравниваются по правилам языка, заданного для информационной базы и сеанса. Настройка базы — в Конфигураторе: Администрирование → Региональные установки информационной базы. Если одно и то же дерево сортируется по-разному в двух базах, первым делом сравните эти настройки.
Данные из файлов и обменов часто приносят пробелы по краям и неразрывные пробелы. Визуально строки одинаковые, а позиции в сортировке разные. Чистите значения через СокрЛП() при заполнении дерева, а неразрывный пробел заменяйте СтрЗаменить(Значение, Символы.НПП, " ").
Дерево на управляемой форме
Реквизит формы типа «Дерево значений» на клиенте представлен как ДанныеФормыДерево. Это другой объект, и методы обычного дерева у него доступны не все. Надёжный путь — отсортировать на сервере, превратив реквизит в обычное дерево и обратно:
&НаСервере
Процедура СортироватьДеревоНаСервере(ПравилоСортировки)
ДеревоОбъект = РеквизитФормыВЗначение("ДеревоТоваров");
ДеревоОбъект.Строки.Сортировать(ПравилоСортировки, Истина);
ЗначениеВРеквизитФормы(ДеревоОбъект, "ДеревоТоваров");
КонецПроцедуры
&НаКлиенте
Процедура СортироватьПоСумме(Команда)
СортироватьДеревоНаСервере("Сумма Убыв, Наименование Возр");
КонецПроцедуры
Что учесть:
- 📡 Каждый вызов — серверный вызов с передачей всего дерева туда и обратно. На дереве в десятки тысяч строк пользователь заметит паузу.
- 📂 После
ЗначениеВРеквизитФормыразвёрнутые узлы на форме сворачиваются. Если это мешает, запомните идентификаторы раскрытых строк до сортировки и разверните их снова через методРазвернуть()элемента формы. - 🧭 Лучше всего сортировать один раз, ещё при заполнении дерева на сервере, а не отдельной командой после.
Скорость сортировки больших деревьев
Встроенный Сортировать() работает на стороне платформы и быстрее любого цикла на встроенном языке. Тормоза почти всегда создаёт не сама сортировка, а то, что происходит вокруг неё.
| Приём | Почему помогает |
|---|---|
Сортировать в запросе через УПОРЯДОЧИТЬ ПО | Порядок задаёт СУБД, коду остаётся готовое дерево |
| Сортировать один раз после заполнения | Повторная сортировка после каждого Добавить() умножает работу на число строк |
| Строковая колонка с представлением вместо ссылки | Не нужно получать представление каждой ссылки при сравнении |
| Типизированные колонки | Не тратится время на сравнение значений разных типов |
| Сортировать на сервере без лишних передач на клиент | Дерево не гоняется между клиентом и сервером ради одной операции |
| Сортировать только видимую ветку | Если пользователь раскрыл одну группу, незачем упорядочивать все |
Ещё один частый источник медленной работы — самописная сортировка «пузырьком» с перестановкой строк. Её пишут, когда не знают про служебную колонку порядка. Почти любой нестандартный порядок можно свести к числовой колонке и одному вызову Сортировать().
Если в рекурсивной процедуре перед сортировкой есть запросы к базе (например, дозаполнение реквизитов), вынесите их за пределы рекурсии. Один запрос на всё дерево работает в разы быстрее, чем запрос на каждую ветку.