Сортировка ДереваЗначений: все уровни, итоги, ссылки и строки

· 9 мин

Содержание

Дерево значений сортируют методом коллекции строк: Дерево.Строки.Сортировать("Наименование", Истина). Первый параметр — список колонок с направлением, второй — сортировать ли вложенные строки. С Истина платформа упорядочит все уровни сразу, и подчинённые строки останутся у своих родителей.

Сам объект ДеревоЗначений метода сортировки не имеет. Метод есть у коллекции Строки — и у корня дерева, и у любой его строки. Поэтому можно отсортировать всё дерево, одну ветку или только верхний уровень. Ниже — синтаксис, сортировка по нескольким колонкам, разные правила для разных уровней, итоги, строки и ссылки, работа на форме и скорость.

Метод Сортировать: синтаксис и как он работает

Полный вызов выглядит так:

КоллекцияСтрок.Сортировать(<Колонки>, <СортироватьВложенные>, <ОбъектСравнения>);
ПараметрТипЧто задаёт
КолонкиСтрокаИмена колонок через запятую, после имени — Возр или Убыв
СортироватьВложенныеБулевоИстина — все уровни ниже, Ложь (по умолчанию) — только эта коллекция
ОбъектСравненияСравнениеЗначенийКак сравнивать ссылки: по представлению, а не по внутреннему идентификатору

Что происходит при вызове:

  • 🔹 строки коллекции переставляются на месте, новое дерево не создаётся;
  • 🔹 вместе со строкой переезжают все её потомки — иерархия не ломается;
  • 🔹 без второго параметра порядок меняется только на том уровне, у которого вызвали метод;
  • 🔹 направление по умолчанию — по возрастанию, Возр можно не писать.

Простейший пример — дерево подразделений, где у каждой строки есть колонка Наименование:

// Только верхний уровень
Дерево.Строки.Сортировать("Наименование");

// Все уровни дерева
Дерево.Строки.Сортировать("Наименование", Истина);

// Одна ветка: дети и внуки третьей строки верхнего уровня
Ветка = Дерево.Строки[2];
Ветка.Строки.Сортировать("Наименование", Истина);

Если дерево собирали вручную через Строки.Добавить(), не сортируйте после каждого добавления. Наполните дерево целиком и вызовите Сортировать один раз в конце.

Сортировка дерева значений по нескольким колонкам

Отдельного метода вроде «установить сортировку» у дерева нет. Сложный порядок задают одной строкой в первом параметре. Колонки перечисляют через запятую, направление указывают для каждой.

Дерево.Строки.Сортировать("ЭтоГруппа Убыв, Наименование", Истина);

Здесь на каждом уровне сначала идут группы (Истина больше Ложь, поэтому Убыв ставит их наверх), а внутри групп и внутри элементов — алфавит. Так выглядит привычный список номенклатуры: папки сверху, товары под ними.

Ещё несколько рабочих комбинаций:

ЗадачаСтрока колонок
Самые крупные суммы сверху, при равенстве — по дате"Сумма Убыв, Дата"
Новые документы первыми"Дата Убыв, Номер Убыв"
Сотрудники по подразделению, затем по ФИО"Подразделение, ФИО"
Код как число, а не как строка"КодЧисло" (служебная колонка, см. ниже)

Ключевые слова можно писать и по-английски: Asc и Desc. Регистр букв не важен.

Своё правило сравнения через служебную колонку

В языке 1С нельзя передать в Сортировать свою функцию сравнения. Если нужен порядок, которого нет в данных, — заведите колонку-ключ, заполните её и сортируйте по ней.

Пример: статусы заказов должны идти не по алфавиту, а по жизненному циклу.

Дерево.Колонки.Добавить("ПорядокСтатуса", Новый ОписаниеТипов("Число"));

Порядок = Новый Соответствие;
Порядок.Вставить("Новый", 1);
Порядок.Вставить("В работе", 2);
Порядок.Вставить("Отгружен", 3);
Порядок.Вставить("Закрыт", 4);

ЗаполнитьКлючСортировки(Дерево.Строки, Порядок);
Дерево.Строки.Сортировать("ПорядокСтатуса, Дата", Истина);

Процедура ЗаполнитьКлючСортировки(СтрокиДерева, Порядок)
	Для Каждого Строка Из СтрокиДерева Цикл
		Ключ = Порядок.Получить(Строка.Статус);
		Строка.ПорядокСтатуса = ?(Ключ = Неопределено, 99, Ключ);
		ЗаполнитьКлючСортировки(Строка.Строки, Порядок);
	КонецЦикла;
КонецПроцедуры

Неизвестный статус получает 99 и уходит в конец, а не в начало. После сортировки колонку можно удалить: Дерево.Колонки.Удалить("ПорядокСтатуса").

Рекурсивная сортировка: разные правила для разных уровней

Флаг СортироватьВложенные = Истина применяет одно правило ко всем уровням. Часто этого мало. Типичный отчёт: на первом уровне склады по алфавиту, на втором — товары по сумме остатка от большей к меньшей.

Решение — своя рекурсивная процедура. На каждом уровне она вызывает Сортировать без флага вложенных, а потом спускается к детям:

Процедура СортироватьПоУровням(СтрокиДерева, Уровень = 0)
	
	Если СтрокиДерева.Количество() < 2 Тогда
		// Сортировать нечего, но у единственной строки могут быть дети
	ИначеЕсли Уровень = 0 Тогда
		СтрокиДерева.Сортировать("Склад");
	Иначе
		СтрокиДерева.Сортировать("Сумма Убыв, Номенклатура");
	КонецЕсли;
	
	Для Каждого Строка Из СтрокиДерева Цикл
		Если Строка.Строки.Количество() > 0 Тогда
			СортироватьПоУровням(Строка.Строки, Уровень + 1);
		КонецЕсли;
	КонецЦикла;
	
КонецПроцедуры

// Вызов
СортироватьПоУровням(Дерево.Строки);

Уровень можно не передавать параметром, а брать у строки: Строка.Уровень() возвращает номер уровня, верхний — 0. Параметр удобнее, когда вы сортируете ветку и считаете её уровень «первым».

Где рекурсия нужнее встроенного флага:

  • 📂 разные колонки сортировки на разных уровнях;
  • 📂 часть веток надо оставить в исходном порядке (например, раздел «Прочее» — как пришёл из запроса);
  • 📂 сортировка только до определённой глубины — добавьте условие Если Уровень < 2.

Не удаляйте и не добавляйте строки в коллекцию внутри того же цикла Для Каждого, которым её обходите. Перестановка методом Сортировать для текущего уровня делается до цикла — так, как в примере выше.

Как отсортировать дерево с итогами и группировками

Дерево с итогами обычно получают из запроса:

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	Остатки.Склад КАК Склад,
|	Остатки.Номенклатура КАК Номенклатура,
|	Остатки.СуммаОстаток КАК Сумма
|ИЗ
|	РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
|ИТОГИ
|	СУММ(Сумма)
|ПО
|	Склад";

Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

Имя регистра здесь условное — подставьте свой. Важно другое: в строке группировки колонка Сумма уже содержит итог по складу. Поэтому одна команда сортирует и группы, и детали:

Дерево.Строки.Сортировать("Сумма Убыв", Истина);

Склады встанут по убыванию общего остатка, а товары внутри каждого склада — по убыванию своей суммы.

Сортировать в запросе или в дереве

СпособКогда подходитОграничения
УПОРЯДОЧИТЬ ПО в запросеПорядок деталей внутри групп, большие выборкиПорядок самих групп по итогу задать сложнее
Сортировать после выгрузкиГруппы по итогам, смена порядка без повторного запросаРаботает в памяти, на очень больших деревьях медленнее
Своя рекурсияРазные правила по уровнямБольше кода, его надо проверять

Строка «Итого» всегда внизу

Если вы добавили в дерево итоговую строку вручную, сортировка утащит её в середину. Лечится служебной колонкой: обычным строкам — 0, итоговой — 1.

Дерево.Строки.Сортировать("ЭтоИтог, Сумма Убыв");

Сортировка по возрастанию ЭтоИтог всегда оставит итоговую строку последней.

Если итоги после правок дерева надо пересчитать, у коллекции строк есть метод Итог: Строка.Строки.Итог("Сумма") вернёт сумму колонки по подчинённым строкам.

Строки, буквы и ссылки: почему порядок «не тот»

Самые частые жалобы на сортировку дерева касаются не метода, а данных в колонках.

Диагностика: выберите симптом

Сортировка по представлению ссылок

Колонка Номенклатура содержит ссылки. Чтобы получить алфавит, а не случайный на вид порядок:

Сравнение = Новый СравнениеЗначений;
Дерево.Строки.Сортировать("Номенклатура", Истина, Сравнение);

Объект СравнениеЗначений сравнивает ссылки по представлению. Цена — платформе приходится получать представление каждой ссылки. На тысячах строк это заметно, об этом в разделе про скорость.

Ключ с ведущими нулями

Артикулы вида «А-2», «А-10», «А-101» в строковой колонке встают как «А-10», «А-101», «А-2». Выделите числовую часть и сортируйте по ней:

Строка.КлючАртикула = СтрШаблон("%1%2",
	Лев(Строка.Артикул, 2),
	Формат(Число(Сред(Строка.Артикул, 3)), "ЧЦ=10; ЧВН=; ЧГ=0"));

Пример рассчитан на артикулы строго формата «буква-дефис-число». Для других шаблонов разбор строки придётся написать под свои данные, а Число() обернуть в Попытка, чтобы мусорный артикул не остановил обработку.

Локаль информационной базы задают в Конфигураторе: Администрирование → Региональные установки информационной базы. Меняйте её только осознанно: от неё зависит не только сортировка, но и формат чисел и дат у всех пользователей.

Сортировка дерева на управляемой форме

На форме дерево хранится как ДанныеФормыДерево. У его коллекций метода Сортировать нет. Стандартный приём — перенести данные на сервер, отсортировать как обычное дерево и вернуть:

&НаСервере
Процедура СортироватьДеревоНаСервере(Колонки)
	Дерево = РеквизитФормыВЗначение("ДеревоТоваров");
	Дерево.Строки.Сортировать(Колонки, Истина);
	ЗначениеВРеквизитФормы(Дерево, "ДеревоТоваров");
КонецПроцедуры

&НаКлиенте
Процедура КомандаСортироватьПоСумме(Команда)
	СортироватьДеревоНаСервере("Сумма Убыв");
КонецПроцедуры

Что учесть:

  • 🖥 после ЗначениеВРеквизитФормы текущая строка и раскрытые узлы сбрасываются — если нужно, запомните идентификатор текущей строки и раскройте узлы заново через Элементы.ДеревоТоваров.Развернуть();
  • 🖥 каждый вызов — это серверный вызов и передача всего дерева, поэтому не сортируйте при каждом изменении ячейки;
  • 🖥 если дерево — реквизит формы обработки или отчёта, удобно хранить текущий порядок в отдельном реквизите и показывать его в заголовке колонки.

Скорость: сортировка больших деревьев

Встроенный Сортировать работает быстро — это код платформы, а не встроенного языка. Тормозит обычно то, что вокруг него.

Что замедляет:

  • 🐢 самописная «пузырьковая» сортировка через Сдвинуть — на тысячах строк она в разы медленнее встроенного метода;
  • 🐢 сортировка внутри цикла наполнения — каждый вызов переставляет всю коллекцию заново;
  • 🐢 СравнениеЗначений по ссылкам на большом объёме — платформа запрашивает представления;
  • 🐢 перенос большого дерева между клиентом и сервером ради одной сортировки.

Что ускоряет:

  1. Задайте порядок деталей в запросе через УПОРЯДОЧИТЬ ПО, а в памяти доупорядочьте только группы.
  2. Вместо СравнениеЗначений выберите в запросе представление: Остатки.Номенклатура.Представление КАК НоменклатураПредставление — и сортируйте по этой строковой колонке.
  3. Сортируйте один раз, после полного наполнения дерева.
  4. Сортируйте ветку, а не всё дерево, если поменялась одна ветка.
  5. Ключи сортировки (числа, нормализованные строки) считайте один раз при наполнении, а не в отдельном проходе.

У дерева значений нет индексов, в отличие от таблицы значений. Если по нему много ищут через НайтиСтроки(..., Истина), иногда выгоднее держать рядом плоскую таблицу с индексом для поиска, а дерево — только для вывода.

Замерить, что именно тормозит, проще всего замером производительности в режиме отладки: он покажет строки кода с наибольшим временем.

Частые вопросы

Можно ли отсортировать дерево значений по колонке-ссылке без лишних запросов к базе?
Да. Выберите в запросе представление ссылки отдельным полем и сортируйте по этой строковой колонке. СравнениеЗначений тогда не нужен.
Сохраняется ли порядок строк с одинаковым значением ключа?
Не полагайтесь на это. Если важен порядок при равных значениях, добавьте вторую колонку, например номер строки из запроса: «Сумма Убыв, НомерПоПорядку».
Как отсортировать только детей одной строки, не трогая остальное дерево?
Вызовите метод у её коллекции: ВыбраннаяСтрока.Строки.Сортировать(“Наименование”, Истина). Остальные ветки и верхний уровень не изменятся.
Можно ли сортировать по колонке, тип которой не задан?
Можно, но в колонке без типа могут лежать значения разных типов, и порядок станет трудно предсказать. Задайте колонке тип через ОписаниеТипов при создании.
Как вернуть исходный порядок после сортировки?
Заранее добавьте колонку с номером строки при наполнении и в нужный момент отсортируйте по ней с флагом вложенных строк.