В повседневной работе программиста 1С часто возникает задача поиска дублей в таблицах значений. Стандартный метод ТЗ.Свернуть() отлично подходит для того, чтобы посчитать количество вхождений и определить, есть ли дубли в принципе. Но что делать, если нужно не просто найти факт дублирования, а показать пользователю номера всех строк, где встречается повторяющееся значение? Например, вывести сообщение: «Код маркировки 'XYZ' найден в строках 10, 53 и 112».
Рассмотрим несколько подходов для решения этой задачи в 1С:Предприятие 7.7, от самых простых до наиболее производительных.
Этот способ является золотой серединой по соотношению сложности реализации и производительности. Он не требует вложенных циклов по всей таблице и работает достаточно быстро на средних объемах данных. Алгоритм состоит из двух основных этапов: сначала определяем, какие именно значения дублируются, а затем находим все их вхождения в исходной таблице.
Разберем по шагам:
ТЗИсходная). Назовем ее ТЗДубли. Это важно, чтобы не изменять исходные данные.ТЗДубли добавляем новую колонку, например, «Повтор», и заполняем ее единицами. Она будет служить счетчиком при сворачивании.ТЗДубли по ключевой колонке (в нашем случае «КодМаркировки»), суммируя значения в колонке «Повтор». В результате в ТЗДубли останутся только уникальные коды маркировки, а в колонке «Повтор» будет указано, сколько раз каждый код встретился в исходной таблице.Повтор = 1, прервать цикл, так как дальше дублей уже не будет.ТЗДубли. Если значение в колонке «Повтор» больше 1, значит, мы нашли дублирующееся значение.ТЗИсходная с помощью метода НайтиЗначение(). Чтобы найти все вхождения, а не только первое, мы будем удалять найденную строку из исходной таблицы и искать снова.Посмотрим на пример кода, основанный на этом алгоритме:
Процедура ПоискНомеровСтрокДублей()
// Предполагаем, что у нас есть ТЗИсходная с колонкой "КодМаркировки"
// 1. Создаем копию
ТЗДубли = СоздатьОбъект("ТаблицаЗначений");
ТЗИсходная.Выгрузить(ТЗДубли, 1); // Выгружаем только структуру
ТЗИсходная.ВыбратьСтроки();
Пока ТЗИсходная.ПолучитьСтроку() = 1 Цикл
ТЗДубли.НоваяСтрока();
ТЗДубли.КодМаркировки = ТЗИсходная.КодМаркировки;
КонецЦикла;
// 2. Добавляем колонку-счетчик
ТЗДубли.НоваяКолонка("Повтор", "Число");
ТЗДубли.Заполнить(1, "Повтор");
// 3. Сворачиваем для подсчета дублей
ТЗДубли.Свернуть("КодМаркировки", "Повтор");
// 4. Сортируем для оптимизации
ТЗДубли.Сортировать("-Повтор");
// Создадим еще одну копию для безопасного поиска с удалением
ТЗДляПоиска = СоздатьОбъект("ТаблицаЗначений");
ТЗИсходная.Выгрузить(ТЗДляПоиска);
ТЗДляПоиска.НоваяКолонка("ИсходныйНомерСтроки", "Число");
Для инд = 1 По ТЗДляПоиска.КоличествоСтрок() Цикл
ТЗДляПоиска.УстановитьЗначение(инд, "ИсходныйНомерСтроки", инд);
КонецЦикла;
// 5. Перебираем свернутую таблицу
ТЗДубли.ВыбратьСтроки();
Пока ТЗДубли.ПолучитьСтроку() = 1 Цикл
// Если дошли до уникальных значений, выходим
Если ТЗДубли.Повтор = 1 Тогда
Прервать;
КонецЕсли;
СписокНомеровСтрок = "";
ИскомыйКод = ТЗДубли.КодМаркировки;
// 6. Ищем все вхождения в копии исходной ТЗ
Пока 1 = 1 Цикл
НомерНайденнойСтроки = 0;
Если ТЗДляПоиска.НайтиЗначение(ИскомыйКод, НомерНайденнойСтроки, "КодМаркировки") = 1 Тогда
// Получаем исходный номер строки, который мы заботливо сохранили
ИсходныйНомер = ТЗДляПоиска.ПолучитьЗначение(НомерНайденнойСтроки, "ИсходныйНомерСтроки");
СписокНомеровСтрок = СписокНомеровСтрок + ?(ПустаяСтрока(СписокНомеровСтрок) = 1, "", ", ") + ИсходныйНомер;
// Удаляем найденную строку, чтобы следующий поиск нашел следующую
ТЗДляПоиска.УдалитьСтроку(НомерНайденнойСтроки);
Иначе
// Больше вхождений нет
Прервать;
КонецЕсли;
КонецЦикла;
Если ПустаяСтрока(СписокНомеровСтрок) = 0 Тогда
Сообщить("Дубли для кода '" + ИскомыйКод + "' найдены в строках: " + СписокНомеровСтрок);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Важный момент: чтобы не потерять оригинальные номера строк при удалении, мы предварительно создаем копию исходной таблицы и добавляем в нее колонку с исходным номером строки. Это делает алгоритм надежным.
Это самый прямолинейный подход, который легко понять и написать. Его суть — сравнить каждую строку с каждой. Однако его главный недостаток — крайне низкая производительность на больших объемах данных (более нескольких сотен строк). Его сложность O(n²), что означает, что при увеличении количества строк в 10 раз время выполнения увеличится в 100 раз.
Логика следующая:
Пример реализации:
// Создаем копию и сворачиваем
КопияТЗ = СоздатьОбъект("ТаблицаЗначений");
ТЗИсходная.Выгрузить(КопияТЗ);
КопияТЗ.НоваяКолонка("КоличествоДублей", "Число");
КопияТЗ.Заполнить(1, "КоличествоДублей");
КопияТЗ.Свернуть("КодМаркировки", "КоличествоДублей");
// Внешний цикл по свернутой ТЗ
КопияТЗ.ВыбратьСтроки();
Пока КопияТЗ.ПолучитьСтроку() = 1 Цикл
Если КопияТЗ.КоличествоДублей > 1 Тогда
Сообщение = "Для кода '" + КопияТЗ.КодМаркировки + "' найдены дубли в строках: ";
НайденыНомера = 0;
// Внутренний цикл по исходной ТЗ
ТЗИсходная.ВыбратьСтроки();
Пока ТЗИсходная.ПолучитьСтроку() = 1 Цикл
Если ТЗИсходная.КодМаркировки = КопияТЗ.КодМаркировки Тогда
Сообщение = Сообщение + ?(НайденыНомера = 1, ", ", "") + ТЗИсходная.НомерСтроки();
НайденыНомера = 1;
КонецЕсли;
КонецЦикла;
Сообщить(Сообщение);
КонецЕсли;
КонецЦикла;
Используйте этот метод с осторожностью и только если уверены, что в таблице будет не более нескольких сотен строк.
Для тех, кто использует внешнюю компоненту 1С++, существует более элегантное и производительное решение с использованием объекта ИндексированнаяТаблицаЗначений. Этот объект предоставляет мощные методы для работы с данными, включая группировку с сохранением информации об исходных строках.
Основное преимущество — метод Группировать(), который после выполнения позволяет получить доступ к свойству ТзПотомки. Это таблица значений, содержащая ссылки на все исходные строки, которые вошли в сгруппированную запись.
Пример кода:
// Предполагается, что ИТЗ - это объект "ИндексированнаяТаблицаЗначений",
// загруженный из исходной ТЗ.
// Группируем по коду маркировки, считаем количество в колонке "Количество"
ИТЗ.Группировать("КодМаркировки", "Количество", 1);
ИТЗ.ВыбратьСтроки();
Пока ИТЗ.ПолучитьСтроку() = 1 Цикл
Если ИТЗ.Количество > 1 Тогда
Сообщение = "Для кода '" + ИТЗ.КодМаркировки + "' найдено " + ИТЗ.ТзПотомки.КоличествоСтрок() + " дублей в строках:";
// Получаем таблицу с исходными строками
Потомки = ИТЗ.ТзПотомки;
Потомки.ВыбратьСтроки();
НомераСтрок = "";
Пока Потомки.ПолучитьСтроку() = 1 Цикл
НомераСтрок = НомераСтрок + ?(ПустаяСтрока(НомераСтрок) = 1, "", ", ") + Потомки.НомерСтроки;
КонецЦикла;
Сообщить(Сообщение + " " + НомераСтрок);
КонецЕсли;
КонецЦикла;
Этот метод является самым быстрым и технологичным, но требует использования сторонних компонент.
Вне зависимости от выбранного технического решения, стоит помнить о главном: лучший способ борьбы с дублями — это не допускать их появления. Вместо того чтобы реализовывать сложную логику поиска ошибок постфактум (например, при закрытии документа), гораздо правильнее встроить проверки на этапе ввода данных.
Например, при добавлении новой строки в табличную часть документа можно сразу проверить, не был ли такой «КодМаркировки» введен ранее. Если был — выдать пользователю предупреждение и не дать добавить строку или спозиционироваться на уже существующей. Это делает интерфейс отзывчивым, предотвращает ошибки и избавляет от необходимости писать сложные процедуры проверки при проведении или закрытии.