При работе с данными в 1С, особенно когда мы имеем дело со структурными кодами, такими как "1.2.3", "1.11.1" или "1.02.3", часто возникает проблема с их корректной сортировкой. Стандартные механизмы сортировки в 1С (как и во многих других системах), по умолчанию, используют лексикографический (строковый) порядок. Это означает, что строки сравниваются символ за символом по их кодам (например, ASCII), что приводит к результатам, не соответствующим интуитивному "человеческому" пониманию числового порядка.
Давайте рассмотрим типичный пример из практики. Предположим, у нас есть следующий список кодов:
При стандартной лексикографической сортировке по возрастанию мы получим:
Такой порядок обусловлен тем, что символ '0' меньше '1', а '1' меньше '2'. Например, "1.02.3" будет стоять перед "1.1.3", потому что '0' (во второй части "02") меньше '1' (во второй части "1"). Аналогично, "1.11.1" окажется после "1.2.11", так как '1' (во второй части "11") меньше '2' (во второй части "2").
Однако, как мы интуитивно ожидаем, правильный порядок (естественная сортировка) должен быть следующим:
Такой порядок подразумевает, что числовые части кода сравниваются как целые числа, а не как отдельные символы. "1.1.3" (где вторая часть "1") должен идти перед "1.02.3" (где вторая часть "2"), а "1.2.11" (где вторая часть "2") — перед "1.11.1" (где вторая часть "11").
Для решения этой проблемы в 1С существуют несколько подходов, каждый из которых имеет свои преимущества и недостатки. Мы подробно рассмотрим каждый из них.
Один из самых распространенных и эффективных способов добиться правильной сортировки без реализации сложного алгоритма — это предварительно привести все числовые части кода к одинаковой длине путем добавления ведущих нулей. Этот метод позволяет стандартной лексикографической сортировке работать корректно, так как все числовые сегменты будут иметь одинаковую длину, и сравнение будет происходить по числовому значению.
Суть подхода заключается в преобразовании каждой числовой компоненты кода (например, "1", "2", "11", "01") в строку фиксированной длины, дополненную ведущими нулями. Например, если мы решили, что каждая часть кода будет занимать три символа, то:
Таким образом, код "1.2.3" будет преобразован в "001.002.003", а "1.11.1" — в "001.011.001". После такого преобразования стандартная строковая сортировка будет давать желаемый результат.
Рассмотрим, как мы можем реализовать эти функции на встроенном языке 1С.
// Функция для дополнения числа ведущими нулями до заданной длины
// Пример: ДополнитьВедущимиНулями(5, 3) вернет "005"
// ДополнитьВедущимиНулями(123, 3) вернет "123"
// ДополнитьВедущимиНулями(1234, 3) вернет "1234" (не обрежет, но и не дополнит)
Функция ДополнитьВедущимиНулями(Знач Число, Знач Длина) Экспорт
Если ТипЗнч(Число) <> Тип("Число") Тогда
Возврат СтрЗаменить(СокрЛП(Число), " ", ""); // Если не число, возвращаем как есть, очистив от пробелов
КонецЕсли;
ПереданноеЧисло = Число; // Сохраняем исходное число
СтрокаЧисла = Формат(ПереданноеЧисло, "ЧГ=0"); // Преобразуем число в строку без группировки
Если СтрДлина(СтрокаЧисла) >= Длина Тогда
Возврат СтрокаЧисла; // Если длина уже достаточна или больше, возвращаем как есть
Иначе
Возврат СтрЗаполнить("0", Длина - СтрДлина(СтрокаЧисла)) + СтрокаЧисла;
КонецЕсли;
КонецФункции
// Функция для нормализации структурного кода
// Пример: НормализоватьСтруктурныйКод("1.2.3", 3) вернет "001.002.003"
// НормализоватьСтруктурныйКод("1.11.1", 3) вернет "001.011.001"
Функция НормализоватьСтруктурныйКод(Знач ИсходныйКод, Знач МаксДлинаСегмента = 3) Экспорт
Если ПустаяСтрока(ИсходныйКод) Тогда
Возврат "";
КонецЕсли;
МассивЧастей = СтрРазделить(ИсходныйКод, ".", Ложь);
МассивНормализованныхЧастей = Новый Массив;
Для Каждого ЧастьКода Из МассивЧастей Цикл
Попытка
ЧисловаяЧасть = Число(ЧастьКода);
МассивНормализованныхЧастей.Добавить(ДополнитьВедущимиНулями(ЧисловаяЧасть, МаксДлинаСегмента));
Исключение
// Если часть не является числом, добавляем ее как есть (например, в кодах "А1.1")
МассивНормализованныхЧастей.Добавить(ЧастьКода);
КонецПопытки;
КонецЦикла;
Возврат СтрСоединить(МассивНормализованныхЧастей, ".");
КонецФункции
После реализации этих функций, наиболее эффективным способом использования нормализованного кода будет хранение его в отдельном реквизите справочника или документа, к которому относится этот код (сообщения 6, 12, 13). Например, если у нас есть справочник "ВидыНоменклатуры" с реквизитом "КодСтруктуры", мы можем добавить реквизит "КодСтруктурыДляСортировки" строкового типа. При записи нового элемента или изменении "КодСтруктуры" мы будем автоматически заполнять "КодСтруктурыДляСортировки" его нормализованным значением.
Давайте посмотрим на пример обновления реквизита при записи объекта:
// В обработчике события "ПередЗаписью" объекта (например, справочника)
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Предположим, что "КодСтруктуры" - это наш исходный строковый реквизит
// А "КодСтруктурыДляСортировки" - это новый реквизит для нормализованного кода
Если ЗначениеЗаполнено(ЭтотОбъект.КодСтруктуры) Тогда
ЭтотОбъект.КодСтруктурыДляСортировки = НормализоватьСтруктурныйКод(ЭтотОбъект.КодСтруктуры, 3);
Иначе
ЭтотОбъект.КодСтруктурыДляСортировки = "";
КонецЕсли;
КонецПроцедуры
Далее, для всех операций сортировки (в запросах, динамических списках, отчетах) мы просто будем использовать реквизит КодСтруктурыДляСортировки вместо исходного КодСтруктуры. Это позволит получать корректную естественную сортировку без дополнительных усилий при каждом запросе данных.
Если мы не хотим или не можем изменять структуру данных (добавлять новый реквизит), или если потребность в такой сортировке возникает нечасто и для относительно небольшого объема данных, можно использовать подход "на лету" с помощью промежуточной ТаблицыЗначений. Этот метод предполагает, что мы разбираем код на числовые части непосредственно перед сортировкой, а затем сортируем по этим числовым частям.
Мы будем загружать наши исходные данные (например, из запроса) в ТаблицуЗначений. Затем для каждой строки этой таблицы мы разобьем строковый код на его числовые компоненты и запишем эти компоненты в отдельные колонки ТаблицыЗначений, но уже как числовые типы. После этого мы можем выполнить сортировку ТаблицыЗначений по этим числовым колонкам.
ТаблицуЗначений.ТаблицуЗначений добавляем новые колонки числового типа для каждой ожидаемой части кода (например, Часть1, Часть2, Часть3). Мы должны заранее знать максимальное количество частей в коде.ТаблицыЗначений. Для каждой строки:
Часть1, Часть2 и т.д.).Сортировать() объекта ТаблицаЗначений, указывая в качестве полей сортировки наши новые числовые колонки (Часть1, Часть2, Часть3) в нужном порядке.ТаблицаЗначений будет содержать данные в желаемом порядке.Рассмотрим пример функции, которая принимает ТаблицуЗначений с колонкой "Код" и возвращает отсортированную ТаблицуЗначений.
// Функция для сортировки таблицы значений по структурному коду
// ИсходнаяТЗ должна содержать колонку с именем "Код", содержащую структурный код
Функция СортироватьПоСтруктурномуКоду(Знач ИсходнаяТЗ) Экспорт
Если ИсходнаяТЗ.Колонки.Найти("Код") = Неопределено Тогда
Возврат ИсходнаяТЗ; // ИсходнаяТЗ не содержит колонки "Код"
КонецЕсли;
// 1. Создаем копию таблицы для работы
РезультатТЗ = ИсходнаяТЗ.Скопировать();
// 2. Добавляем колонки для числовых частей кода
// Предполагаем, что код может иметь до 5 частей (можно настроить)
МаксКоличествоЧастей = 5;
Для НомерЧасти = 1 По МаксКоличествоЧастей Цикл
ИмяКолонки = "ЧастьКода" + НомерЧасти;
Если РезультатТЗ.Колонки.Найти(ИмяКолонки) = Неопределено Тогда
РезультатТЗ.Колонки.Добавить(ИмяКолонки, Тип("Число"));
КонецЕсли;
КонецЦикла;
// 3. Заполняем числовые колонки
Для Каждого СтрокаТЗ Из РезультатТЗ Цикл
КодСтроки = СтрокаТЗ.Код;
Если ПустаяСтрока(КодСтроки) Тогда
Продолжить;
КонецЕсли;
МассивЧастей = СтрРазделить(КодСтроки, ".", Ложь);
Для Индекс = 0 По Мин(МассивЧастей.ВВерхнейГранице(), МаксКоличествоЧастей - 1) Цикл
ИмяКолонки = "ЧастьКода" + (Индекс + 1);
Попытка
СтрокаТЗ[ИмяКолонки] = Число(МассивЧастей[Индекс]);
Исключение
СтрокаТЗ[ИмяКолонки] = 0; // В случае ошибки преобразования (например, нечисловая часть)
КонецПопытки;
КонецЦикла;
КонецЦикла;
// 4. Сортируем ТаблицуЗначений по числовым колонкам
СтрокаПорядка = "";
Для НомерЧасти = 1 По МаксКоличествоЧастей Цикл
Если Не ПустаяСтрока(СтрокаПорядка) Тогда
СтрокаПорядка = СтрокаПорядка + ", ";
КонецЕсли;
СтрокаПорядка = СтрокаПорядка + "ЧастьКода" + НомерЧасти + " ВОЗР";
КонецЦикла;
РезультатТЗ.Сортировать(СтрокаПорядка);
// 5. Удаляем временные колонки, если они больше не нужны
Для НомерЧасти = 1 По МаксКоличествоЧастей Цикл
ИмяКолонки = "ЧастьКода" + НомерЧасти;
РезультатТЗ.Колонки.Удалить(ИмяКолонки);
КонецЦикла;
Возврат РезультатТЗ;
КонецФункции
// Пример использования:
// Запрос = Новый Запрос;
// Запрос.Текст = "ВЫБРАТЬ Код ИЗ Справочник.Номенклатура";
// ИсходныеДанные = Запрос.Выполнить().Выгрузить();
// ОтсортированныеДанные = СортироватьПоСтруктурномуКоду(ИсходныеДанные);
// Для Каждого Строка Из ОтсортированныеДанные Цикл
// Сообщить(Строка.Код);
// КонецЦикла;
Третий подход, который является наиболее универсальным и не требует изменения структуры данных, — это реализация собственного алгоритма естественной сортировки (Natsort). Естественная сортировка — это алгоритм, который упорядочивает текст, содержащий числа, таким образом, чтобы это соответствовало человеческому восприятию. Она обрабатывает числовые последовательности внутри строки как числовые значения, а не как отдельные символы.
В основе естественной сортировки лежит покомпонентное сравнение. Для двух сравниваемых строк алгоритм выполняет следующие действия:
В 1С нет встроенной функции для естественной сортировки, но мы можем реализовать ее на встроенном языке.
Реализация естественной сортировки в 1С более сложна и объемна. Мы рассмотрим основные блоки, которые нам понадобятся.
Эта функция будет извлекать из строки последовательности цифр и не-цифр.
// Вспомогательная функция для разделения строки на числовые и строковые компоненты
// Пример: "1.02.3" -> [1, ".", 2, ".", 3]
// Пример: "Строка10" -> ["Строка", 10]
Функция РазделитьНаКомпоненты(Знач СтрокаКода) Экспорт
МассивКомпонентов = Новый Массив;
ТекущаяПозиция = 1;
ДлинаСтроки = СтрДлина(СтрокаКода);
Пока ТекущаяПозиция <= ДлинаСтроки Цикл
Символ = Сред(СтрокаКода, ТекущаяПозиция, 1);
Если СтрНайти("0123456789", Символ) > 0 Тогда // Если символ - цифра
НачалоЧисла = ТекущаяПозиция;
Пока ТекущаяПозиция <= ДлинаСтроки И СтрНайти("0123456789", Сред(СтрокаКода, ТекущаяПозиция, 1)) > 0 Цикл
ТекущаяПозиция = ТекущаяПозиция + 1;
КонецЦикла;
ЧисловаяЧасть = Сред(СтрокаКода, НачалоЧисла, ТекущаяПозиция - НачалоЧисла);
МассивКомпонентов.Добавить(Число(ЧисловаяЧасть)); // Добавляем число
Иначе // Если символ - не цифра
НачалоСтроки = ТекущаяПозиция;
Пока ТекущаяПозиция <= ДлинаСтроки И СтрНайти("0123456789", Сред(СтрокаКода, ТекущаяПозиция, 1)) = 0 Цикл
ТекущаяПозиция = ТекущаяПозиция + 1;
КонецЦикла;
СтроковаяЧасть = Сред(СтрокаКода, НачалоСтроки, ТекущаяПозиция - НачалоСтроки);
МассивКомпонентов.Добавить(СтроковаяЧасть); // Добавляем строковую часть
КонецЕсли;
КонецЦикла;
Возврат МассивКомпонентов;
КонецФункции
Эта функция будет принимать два кода и возвращать результат сравнения, необходимый для алгоритмов сортировки.
// Функция-компаратор для естественной сортировки двух структурных кодов
// Возвращает:
// -1, если Код1 < Код2
// 0, если Код1 = Код2
// 1, если Код1 > Код2
Функция СравнитьСтруктурныеКодыЕстественно(Знач Код1, Знач Код2) Экспорт
Если Код1 = Код2 Тогда
Возврат 0;
КонецЕсли;
Если ПустаяСтрока(Код1) Тогда
Возврат -1; // Пустая строка меньше заполненной
КонецЕсли;
Если ПустаяСтрока(Код2) Тогда
Возврат 1; // Заполненная строка больше пустой
КонецЕсли;
МассивКомпонентов1 = РазделитьНаКомпоненты(Код1);
МассивКомпонентов2 = РазделитьНаКомпоненты(Код2);
Индекс1 = 0;
Индекс2 = 0;
Пока Истина Цикл
Компонент1 = Неопределено;
Если Индекс1 <= МассивКомпонентов1.ВВерхнейГранице() Тогда
Компонент1 = МассивКомпонентов1[Индекс1];
КонецЕсли;
Компонент2 = Неопределено;
Если Индекс2 <= МассивКомпонентов2.ВВерхнейГранице() Тогда
Компонент2 = МассивКомпонентов2[Индекс2];
КонецЕсли;
Если Компонент1 = Неопределено И Компонент2 = Неопределено Тогда
Возврат 0; // Коды равны, достигли конца обоих
ИначеЕсли Компонент1 = Неопределено Тогда
Возврат -1; // Код1 короче Код2, но части совпали до этого
ИначеЕсли Компонент2 = Неопределено Тогда
Возврат 1; // Код1 длиннее Код2, но части совпали до этого
КонецЕсли;
Тип1 = ТипЗнч(Компонент1);
Тип2 = ТипЗнч(Компонент2);
Если Тип1 = Тип("Число") И Тип2 = Тип("Число") Тогда
Если Компонент1 < Компонент2 Тогда
Возврат -1;
ИначеЕсли Компонент1 > Компонент2 Тогда
Возврат 1;
КонецЕсли;
ИначеЕсли Тип1 = Тип("Строка") И Тип2 = Тип("Строка") Тогда
Если Компонент1 < Компонент2 Тогда
Возврат -1;
ИначеЕсли Компонент1 > Компонент2 Тогда
Возврат 1;
КонецЕсли;
Иначе
// Сравнение разнотипных компонентов: число всегда "меньше" строки
Если Тип1 = Тип("Число") Тогда // Число1 vs Строка2
Возврат -1;
Иначе // Строка1 vs Число2
Возврат 1;
КонецЕсли;
КонецЕсли;
Индекс1 = Индекс1 + 1;
Индекс2 = Индекс2 + 1;
КонецЦикла;
КонецФункции
В 1С, к сожалению, нет прямого способа передать пользовательскую функцию сравнения для методов Сортировать() у большинства коллекций (например, Массив). Мы можем использовать этот компаратор либо для сортировки ТаблицыЗначений (как в Подходе 2, но вместо добавления числовых колонок, мы будем иметь только одну колонку для исходного кода и использовать компаратор при сравнении), либо для реализации собственного алгоритма сортировки для массива объектов.
Например, мы можем перенести наши объекты в Массив, который содержит объекты с реквизитом "Код", и реализовать сортировку "пузырьком" или "быстрой сортировкой", используя нашу функцию СравнитьСтруктурныеКодыЕстественно().
// Пример использования компаратора для сортировки МассиваСтрок
Функция ОтсортироватьМассивКодов(Знач МассивКодов) Экспорт
// Простейшая сортировка пузырьком для демонстрации
Для И1 = 0 По МассивКодов.ВВерхнейГранице() Цикл
Для И2 = И1 + 1 По МассивКодов.ВВерхнейГранице() Цикл
Если СравнитьСтруктурныеКодыЕстественно(МассивКодов[И1], МассивКодов[И2]) = 1 Тогда
// Меняем местами, если первый больше второго
ВременнаяПеременная = МассивКодов[И1];
МассивКодов[И1] = МассивКодов[И2];
МассивКодов[И2] = ВременнаяПеременная;
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат МассивКодов;
КонецФункции
// Пример:
// ИсходныйМассив = Новый Массив;
// ИсходныйМассив.Добавить("1.01.01");
// ИсходныйМассив.Добавить("1.02.3");
// ИсходныйМассив.Добавить("1.1.3");
// ИсходныйМассив.Добавить("1.11.1");
// ИсходныйМассив.Добавить("1.2.11");
//
// ОтсортированныйМассив = ОтсортироватьМассивКодов(ИсходныйМассив);
// Для Каждого Код Из ОтсортированныйМассив Цикл
// Сообщить(Код);
// КонецЦикла;
Помимо технических решений для сортировки, крайне важно учитывать и качество исходных данных. Многие проблемы с сортировкой возникают из-за непоследовательного или некорректного формирования структурных кодов на этапе их ввода или генерации. В сообщениях форума (5, 7) справедливо отмечается, что за коды "01" и "1" на одном уровне "надо отрывать кое что кое кому", подчеркивая важность единообразия.
Для предотвращения проблем с сортировкой изначально, мы рекомендуем следовать следующим правилам при проектировании и вводе структурных кодов:
Соблюдение этих рекомендаций позволит избежать возникновения проблем с сортировкой на ранних этапах и значительно упростит работу с данными в будущем.
Таким образом, мы рассмотрели три основных подхода к решению проблемы некорректной сортировки структурного кода в 1С. Выбор конкретного подхода зависит от требований к производительности, объему данных, возможности изменения структуры метаданных и сложности требуемой реализации. В большинстве случаев, для часто используемых кодов, хранение нормализованного кода в отдельном реквизите является наиболее оптимальным решением. Если же требуется гибкость и работа с данными "на лету" без изменения структуры, можно прибегнуть к сортировке через ТаблицуЗначений или реализации алгоритма естественной сортировки.