Порядок строк в ДеревоЗначений: от одной колонки до правил по уровням

· 10 мин

Содержание

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

Этого хватает в большинстве простых задач. Сложности начинаются, когда группы надо выстроить по сумме, уровни — по разным правилам, а колонка хранит ссылки или числа в виде строк. Каждый такой случай ниже разобран с рабочим кодом.

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

У дерева нет одной плоской таблицы. Каждая строка хранит свою коллекцию подчинённых строк — свойство Строки. Поэтому метод сортировки вызывают не у самого дерева, а у коллекции строк нужного уровня.

Параметры метода:

ПараметрЧто передатьПо умолчанию
КолонкиСтрока: имена колонок через запятую, после имени — Возр или Убыв— (обязателен)
Сортировать вложенныеИстина — упорядочить все уровни нижеЛожь
Объект сравненияНовый СравнениеЗначенийне задан

Соберём небольшое дерево и отсортируем его:

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

Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Склад Б";

Строка = Группа.Строки.Добавить();
Строка.Наименование = "Цемент";
Строка.Сумма = 90000;

Строка = Группа.Строки.Добавить();
Строка.Наименование = "Арматура";
Строка.Сумма = 80000;

Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Склад А";

Строка = Группа.Строки.Добавить();
Строка.Наименование = "Кирпич";
Строка.Сумма = 120000;

Строка = Группа.Строки.Добавить();
Строка.Наименование = "Песок";
Строка.Сумма = 30000;

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

Результат: «Склад А» (Кирпич, Песок), затем «Склад Б» (Арматура, Цемент).

Ключевые моменты:

  • 🌳 Иерархия сохраняется. Строка никогда не переезжает к чужому родителю — сортируются только «братья» внутри одной группы.
  • 🔁 С Истина метод сам обходит все уровни. Писать рекурсию для одинакового правила не нужно.
  • 🎯 Можно сортировать одну ветку: Группа.Строки.Сортировать("Сумма Убыв") — остальное дерево не тронется.

Направление Возр можно не писать — оно применяется по умолчанию. Но в коде с несколькими колонками лучше указывать явно: так правило читается без справки.

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

Сложное правило задаётся одной строкой. Колонки перечисляют через запятую в порядке приоритета. Вторая колонка работает только там, где значения первой совпали.

// Сначала свежие даты, при равной дате — большие суммы
Дерево.Строки.Сортировать("Дата Убыв, Сумма Убыв", Истина);

Строку с правилом удобно собирать из настроек, а не держать в коде. Например, пользователь выбрал колонку и направление в форме отчёта:

Функция ПравилоСортировки(ИмяКолонки, ПоУбыванию)
    Направление = ?(ПоУбыванию, "Убыв", "Возр");
    // Последняя колонка — Наименование: порядок при равных значениях всегда одинаковый
    Возврат СтрШаблон("%1 %2, Наименование Возр", ИмяКолонки, Направление);
КонецФункции

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

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

Свой порядок, которого нет в данных

Бывает, что нужный порядок не алфавитный и не числовой: группа «Прочее» всегда в конце, строка «Итого» — внизу. Его не вычислить из имени. Решение — служебная колонка с номером:

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

Для Каждого СтрокаДерева Из Дерево.Строки Цикл
    Если СтрокаДерева.Наименование = "Прочее" Тогда
        СтрокаДерева.Порядок = 1;
    Иначе
        СтрокаДерева.Порядок = 0;
    КонецЕсли;
КонецЦикла;

Дерево.Строки.Сортировать("Порядок Возр, Наименование Возр");

Колонку Порядок потом можно скрыть на форме или не выводить в макет. Пользователь её не видит, а порядок держится.

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

Параметр Истина применяет одно правило ко всем уровням. А в отчётах часто нужно другое: склады — по алфавиту, документы внутри склада — от новых к старым. Здесь без собственной рекурсии не обойтись.

Процедура СортироватьПоУровням(КоллекцияСтрок, Уровень)

    Если Уровень = 0 Тогда
        КоллекцияСтрок.Сортировать("Наименование Возр");
    Иначе
        КоллекцияСтрок.Сортировать("Дата Убыв, Сумма Убыв");
    КонецЕсли;

    Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
        Если СтрокаДерева.Строки.Количество() > 0 Тогда
            СортироватьПоУровням(СтрокаДерева.Строки, Уровень + 1);
        КонецЕсли;
    КонецЦикла;

КонецПроцедуры

СортироватьПоУровням(Дерево.Строки, 0);

Как это работает:

  1. Процедура получает коллекцию одного уровня и сортирует её без второго параметра — только этот уровень.
  2. Затем обходит строки и для каждой непустой ветки вызывает себя же, увеличив номер уровня.
  3. Обход идёт по уже отсортированной коллекции, а сортируются вложенные коллекции — поэтому цикл ничего не ломает.

У строки дерева есть и собственный метод Уровень(), он возвращает глубину строки (верхний уровень — 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» начинается с единицы.

Два способа исправить:

  1. Хранить номер в отдельной числовой колонке и сортировать по ней — самый чистый вариант.
  2. Если колонка обязана быть строкой, дополнить номер нулями слева: Формат(Номер, "ЧЦ=6; ЧВН=; ЧГ=0") даст «000010». Без ЧГ=0 в длинные числа попадёт разделитель групп разрядов.

Ссылки на справочники и документы

По колонке со ссылками порядок без третьего параметра не совпадает с алфавитом представлений. Для пользователя это выглядит как случайная перестановка.

  • 🔤 Передайте объект сравнения: Строки.Сортировать("Контрагент", Истина, Новый СравнениеЗначений). Ссылки будут сравниваться по представлению.
  • ⚡ На больших объёмах надёжнее добавить строковую колонку с представлением и заполнить её сразу в запросе через ПРЕДСТАВЛЕНИЕ(Данные.Контрагент). Тогда при сортировке не нужно получать представление каждой ссылки.

Смешанные типы в одной колонке

Если колонку создали без ОписаниеТипов, в неё может попасть что угодно: число, строка, Неопределено. Значения разных типов сначала группируются по типу, и строка «Итого» внезапно оказывается среди чисел. Задавайте тип при создании колонки — как в примерах выше.

Буква «ё», регистр и языковые настройки

Строки сравниваются по правилам языка, заданного для информационной базы и сеанса. Настройка базы — в Конфигураторе: Администрирование → Региональные установки информационной базы. Если одно и то же дерево сортируется по-разному в двух базах, первым делом сравните эти настройки.

Данные из файлов и обменов часто приносят пробелы по краям и неразрывные пробелы. Визуально строки одинаковые, а позиции в сортировке разные. Чистите значения через СокрЛП() при заполнении дерева, а неразрывный пробел заменяйте СтрЗаменить(Значение, Символы.НПП, " ").

Дерево на управляемой форме

Реквизит формы типа «Дерево значений» на клиенте представлен как ДанныеФормыДерево. Это другой объект, и методы обычного дерева у него доступны не все. Надёжный путь — отсортировать на сервере, превратив реквизит в обычное дерево и обратно:

&НаСервере
Процедура СортироватьДеревоНаСервере(ПравилоСортировки)

    ДеревоОбъект = РеквизитФормыВЗначение("ДеревоТоваров");
    ДеревоОбъект.Строки.Сортировать(ПравилоСортировки, Истина);
    ЗначениеВРеквизитФормы(ДеревоОбъект, "ДеревоТоваров");

КонецПроцедуры

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

Что учесть:

  • 📡 Каждый вызов — серверный вызов с передачей всего дерева туда и обратно. На дереве в десятки тысяч строк пользователь заметит паузу.
  • 📂 После ЗначениеВРеквизитФормы развёрнутые узлы на форме сворачиваются. Если это мешает, запомните идентификаторы раскрытых строк до сортировки и разверните их снова через метод Развернуть() элемента формы.
  • 🧭 Лучше всего сортировать один раз, ещё при заполнении дерева на сервере, а не отдельной командой после.

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

Встроенный Сортировать() работает на стороне платформы и быстрее любого цикла на встроенном языке. Тормоза почти всегда создаёт не сама сортировка, а то, что происходит вокруг неё.

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

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

Если в рекурсивной процедуре перед сортировкой есть запросы к базе (например, дозаполнение реквизитов), вынесите их за пределы рекурсии. Один запрос на всё дерево работает в разы быстрее, чем запрос на каждую ветку.

Ошибки, из-за которых сортировка не срабатывает

Диагностика: выберите симптом
Можно ли отсортировать дерево значений по колонке, которой нет на форме?
Да. Сортировка работает по любой колонке дерева, даже если элемент формы её не показывает. На этом и строится приём со служебной колонкой порядка.
Чем сортировка дерева отличается от сортировки таблицы значений?
Синтаксис колонок тот же, но у дерева сортируются строки внутри каждого родителя отдельно, а для вложенных уровней нужен второй параметр. Строки между группами не перемещаются.
Как отсортировать дерево по убыванию даты, оставив пустые даты внизу?
Пустая дата меньше любой заполненной, поэтому при «Убыв» она и так окажется внизу. При «Возр» добавьте служебную колонку: 1 для пустых дат, 0 для остальных, и поставьте её первой в правиле.
Сохранится ли порядок, если потом добавить строку?
Нет. Новая строка встаёт в конец своей коллекции. Либо вызовите сортировку ещё раз, либо вставьте строку на нужное место методом Вставить() коллекции строк.
Нужно ли сортировать дерево, если оно выгружено из запроса с УПОРЯДОЧИТЬ ПО?
Нет, если порядок из запроса вас устраивает. Досортировка в коде нужна, когда порядок зависит от итогов групп или выбирается пользователем после формирования.