Как найти и показать номера всех строк-дублей в таблице значений 1С 7.7?

Программист 1С v7.7
← На главную

В повседневной работе программиста 1С часто возникает задача поиска дублей в таблицах значений. Стандартный метод ТЗ.Свернуть() отлично подходит для того, чтобы посчитать количество вхождений и определить, есть ли дубли в принципе. Но что делать, если нужно не просто найти факт дублирования, а показать пользователю номера всех строк, где встречается повторяющееся значение? Например, вывести сообщение: «Код маркировки 'XYZ' найден в строках 10, 53 и 112».

Рассмотрим несколько подходов для решения этой задачи в 1С:Предприятие 7.7, от самых простых до наиболее производительных.

Решение 1: Эффективный метод с использованием `Свернуть()` и `НайтиЗначение()`

Этот способ является золотой серединой по соотношению сложности реализации и производительности. Он не требует вложенных циклов по всей таблице и работает достаточно быстро на средних объемах данных. Алгоритм состоит из двух основных этапов: сначала определяем, какие именно значения дублируются, а затем находим все их вхождения в исходной таблице.

Разберем по шагам:

  1. Создаем полную копию нашей исходной таблицы значений (ТЗИсходная). Назовем ее ТЗДубли. Это важно, чтобы не изменять исходные данные.
  2. В таблицу ТЗДубли добавляем новую колонку, например, «Повтор», и заполняем ее единицами. Она будет служить счетчиком при сворачивании.
  3. Сворачиваем таблицу ТЗДубли по ключевой колонке (в нашем случае «КодМаркировки»), суммируя значения в колонке «Повтор». В результате в ТЗДубли останутся только уникальные коды маркировки, а в колонке «Повтор» будет указано, сколько раз каждый код встретился в исходной таблице.
  4. Сортируем свернутую таблицу по убыванию колонки «Повтор». Это удобный трюк, который позволяет нам сначала обработать все строки с дублями, а затем, как только мы встретим строку с Повтор = 1, прервать цикл, так как дальше дублей уже не будет.
  5. Перебираем отсортированную таблицу ТЗДубли. Если значение в колонке «Повтор» больше 1, значит, мы нашли дублирующееся значение.
  6. Для каждого такого значения организуем цикл поиска в исходной таблице ТЗИсходная с помощью метода НайтиЗначение(). Чтобы найти все вхождения, а не только первое, мы будем удалять найденную строку из исходной таблицы и искать снова.

Посмотрим на пример кода, основанный на этом алгоритме:


Процедура ПоискНомеровСтрокДублей()
    // Предполагаем, что у нас есть ТЗИсходная с колонкой "КодМаркировки"
    
    // 1. Создаем копию
    ТЗДубли = СоздатьОбъект("ТаблицаЗначений");
    ТЗИсходная.Выгрузить(ТЗДубли, 1); // Выгружаем только структуру
    ТЗИсходная.ВыбратьСтроки();
    Пока ТЗИсходная.ПолучитьСтроку() = 1 Цикл
        ТЗДубли.НоваяСтрока();
        ТЗДубли.КодМаркировки = ТЗИсходная.КодМаркировки;
    КонецЦикла;
    
    // 2. Добавляем колонку-счетчик
    ТЗДубли.НоваяКолонка("Повтор", "Число");
    ТЗДубли.Заполнить(1, "Повтор");
    
    // 3. Сворачиваем для подсчета дублей
    ТЗДубли.Свернуть("КодМаркировки", "Повтор");
    
    // 4. Сортируем для оптимизации
    ТЗДубли.Сортировать("-Повтор");

    // Создадим еще одну копию для безопасного поиска с удалением
    ТЗДляПоиска = СоздатьОбъект("ТаблицаЗначений");
    ТЗИсходная.Выгрузить(ТЗДляПоиска);
    ТЗДляПоиска.НоваяКолонка("ИсходныйНомерСтроки", "Число");
    Для инд = 1 По ТЗДляПоиска.КоличествоСтрок() Цикл
        ТЗДляПоиска.УстановитьЗначение(инд, "ИсходныйНомерСтроки", инд);
    КонецЦикла;
    
    // 5. Перебираем свернутую таблицу
    ТЗДубли.ВыбратьСтроки();
    Пока ТЗДубли.ПолучитьСтроку() = 1 Цикл
        // Если дошли до уникальных значений, выходим
        Если ТЗДубли.Повтор = 1 Тогда
            Прервать;
        КонецЕсли;
        
        СписокНомеровСтрок = "";
        ИскомыйКод = ТЗДубли.КодМаркировки;
        
        // 6. Ищем все вхождения в копии исходной ТЗ
        Пока 1 = 1 Цикл
            НомерНайденнойСтроки = 0;
            Если ТЗДляПоиска.НайтиЗначение(ИскомыйКод, НомерНайденнойСтроки, "КодМаркировки") = 1 Тогда
                // Получаем исходный номер строки, который мы заботливо сохранили
                ИсходныйНомер = ТЗДляПоиска.ПолучитьЗначение(НомерНайденнойСтроки, "ИсходныйНомерСтроки");
                СписокНомеровСтрок = СписокНомеровСтрок + ?(ПустаяСтрока(СписокНомеровСтрок) = 1, "", ", ") + ИсходныйНомер;
                
                // Удаляем найденную строку, чтобы следующий поиск нашел следующую
                ТЗДляПоиска.УдалитьСтроку(НомерНайденнойСтроки);
            Иначе
                // Больше вхождений нет
                Прервать;
            КонецЕсли;
        КонецЦикла;
        
        Если ПустаяСтрока(СписокНомеровСтрок) = 0 Тогда
            Сообщить("Дубли для кода '" + ИскомыйКод + "' найдены в строках: " + СписокНомеровСтрок);
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

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

Решение 2: Метод вложенных циклов (простой, но медленный)

Это самый прямолинейный подход, который легко понять и написать. Его суть — сравнить каждую строку с каждой. Однако его главный недостаток — крайне низкая производительность на больших объемах данных (более нескольких сотен строк). Его сложность O(n²), что означает, что при увеличении количества строк в 10 раз время выполнения увеличится в 100 раз.

Логика следующая:

  1. Создаем копию таблицы и сворачиваем ее, чтобы получить список уникальных значений, которые имеют дубли (где количество > 1).
  2. Запускаем внешний цикл по этой небольшой свернутой таблице.
  3. Для каждого значения из внешнего цикла запускаем внутренний цикл по всей исходной таблице.
  4. Внутри сравниваем значение из свернутой таблицы со значением в текущей строке исходной таблицы. Если они совпадают, выводим номер строки.

Пример реализации:


// Создаем копию и сворачиваем
КопияТЗ = СоздатьОбъект("ТаблицаЗначений");
ТЗИсходная.Выгрузить(КопияТЗ);
КопияТЗ.НоваяКолонка("КоличествоДублей", "Число");
КопияТЗ.Заполнить(1, "КоличествоДублей");
КопияТЗ.Свернуть("КодМаркировки", "КоличествоДублей");

// Внешний цикл по свернутой ТЗ
КопияТЗ.ВыбратьСтроки();
Пока КопияТЗ.ПолучитьСтроку() = 1 Цикл
    Если КопияТЗ.КоличествоДублей > 1 Тогда
        Сообщение = "Для кода '" + КопияТЗ.КодМаркировки + "' найдены дубли в строках: ";
        НайденыНомера = 0;
        
        // Внутренний цикл по исходной ТЗ
        ТЗИсходная.ВыбратьСтроки();
        Пока ТЗИсходная.ПолучитьСтроку() = 1 Цикл
            Если ТЗИсходная.КодМаркировки = КопияТЗ.КодМаркировки Тогда
                Сообщение = Сообщение + ?(НайденыНомера = 1, ", ", "") + ТЗИсходная.НомерСтроки();
                НайденыНомера = 1;
            КонецЕсли;
        КонецЦикла;
        Сообщить(Сообщение);
    КонецЕсли;
КонецЦикла;

Используйте этот метод с осторожностью и только если уверены, что в таблице будет не более нескольких сотен строк.

Решение 3: Продвинутый подход с использованием 1С++

Для тех, кто использует внешнюю компоненту 1С++, существует более элегантное и производительное решение с использованием объекта ИндексированнаяТаблицаЗначений. Этот объект предоставляет мощные методы для работы с данными, включая группировку с сохранением информации об исходных строках.

Основное преимущество — метод Группировать(), который после выполнения позволяет получить доступ к свойству ТзПотомки. Это таблица значений, содержащая ссылки на все исходные строки, которые вошли в сгруппированную запись.

Пример кода:


// Предполагается, что ИТЗ - это объект "ИндексированнаяТаблицаЗначений",
// загруженный из исходной ТЗ.

// Группируем по коду маркировки, считаем количество в колонке "Количество"
ИТЗ.Группировать("КодМаркировки", "Количество", 1);

ИТЗ.ВыбратьСтроки();
Пока ИТЗ.ПолучитьСтроку() = 1 Цикл
    Если ИТЗ.Количество > 1 Тогда
        Сообщение = "Для кода '" + ИТЗ.КодМаркировки + "' найдено " + ИТЗ.ТзПотомки.КоличествоСтрок() + " дублей в строках:";
        
        // Получаем таблицу с исходными строками
        Потомки = ИТЗ.ТзПотомки;
        Потомки.ВыбратьСтроки();
        НомераСтрок = "";
        Пока Потомки.ПолучитьСтроку() = 1 Цикл
            НомераСтрок = НомераСтрок + ?(ПустаяСтрока(НомераСтрок) = 1, "", ", ") + Потомки.НомерСтроки;
        КонецЦикла;
        
        Сообщить(Сообщение + " " + НомераСтрок);
    КонецЕсли;
КонецЦикла;

Этот метод является самым быстрым и технологичным, но требует использования сторонних компонент.

Важный архитектурный совет: Предотвращайте дубли, а не ищите их

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

Например, при добавлении новой строки в табличную часть документа можно сразу проверить, не был ли такой «КодМаркировки» введен ранее. Если был — выдать пользователю предупреждение и не дать добавить строку или спозиционироваться на уже существующей. Это делает интерфейс отзывчивым, предотвращает ошибки и избавляет от необходимости писать сложные процедуры проверки при проведении или закрытии.

← На главную