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

Программист 1С v8.3 (Управляемые формы) Управленческий учет
← На главную

В практике разработчика 1С часто возникает задача очистки данных или корректировки движений, когда в таблице значений (ТЗ) присутствуют записи, которые фактически аннулируют друг друга. Типичный пример: две строки по одному и тому же аналитическому разрезу (например, подотчетное лицо или подразделение), где суммы равны по модулю, но имеют разные знаки (например, 100 и -100). Такие строки необходимо найти и «схлопнуть», чтобы они не искажали итоговые обороты. Рассмотрим подробнее несколько способов решения этой задачи, от простых переборов до оптимизированных алгоритмов.

Метод 1: Использование вспомогательной колонки и индексации

Проанализируем самый наглядный способ, который подходит для небольших и средних таблиц. Суть заключается в том, чтобы помечать уже «обработанные» строки, чтобы не использовать их повторно в других парах. Разберем этот процесс по шагам:

  1. Добавим в таблицу значений новую колонку Обработано с типом Булево.
  2. Для ускорения поиска создадим индекс по колонке Сумма.
  3. Запустим цикл по таблице. Для каждой строки, которая еще не помечена как обработанная, будем искать другую строку с суммой -ТекСтрока.Сумма.

Посмотрим на пример кода, реализующего эту логику:


// Добавляем служебную колонку
ТЗ.Колонки.Добавить("Обработано", Новый ОписаниеТипов("Булево"));
ТЗ.Индексы.Добавить("Сумма");

Для Каждого СтрокаИзм Из ТЗ Цикл
    Если СтрокаИзм.Обработано Тогда
        Продолжить;
    КонецЕсли;
    
    // Параметры поиска: ищем противоположную сумму, которая еще не занята
    ПараметрыОтбора = Новый Структура;
    ПараметрыОтбора.Вставить("Сумма", -СтрокаИзм.Сумма);
    ПараметрыОтбора.Вставить("Обработано", Ложь);
    
    НайденныеСтроки = ТЗ.НайтиСтроки(ПараметрыОтбора);
    
    Для Каждого Кандидат Из НайденныеСтроки Цикл
        // Проверяем, что это не та же самая строка (актуально для нулевых сумм)
        Если Кандидат = СтрокаИзм Тогда
            Продолжить;
        КонецЕсли;
        
        // Если нашли подходящую пару, помечаем обе строки
        СтрокаИзм.Обработано = Истина;
        Кандидат.Обработано = Истина;
        Прервать; // Переходим к следующей необработанной строке
    КонецЦикла;
КонецЦикла;

После выполнения этого кода все строки, у которых Обработано равно Истина, можно либо удалить, либо использовать для формирования сторнирующих записей.

Метод 2: Обработка данных с помощью запроса

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

Рассмотрим пример запроса:


ВЫБРАТЬ
    ТЗ.Номер КАК Номер,
    ТЗ.ПодотчетноеЛицо КАК ПодотчетноеЛицо,
    ТЗ.Сумма КАК Сумма
ПОМЕСТИТЬ ВТ_Данные
ИЗ
    &ВнешняяТЗ КАК ТЗ;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ1.Номер КАК НомерПлюс,
    ВТ2.Номер КАК НомерМинус,
    ВТ1.Сумма КАК Сумма
ИЗ
    ВТ_Данные КАК ВТ1
        ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Данные КАК ВТ2
        ПО ВТ1.ПодотчетноеЛицо = ВТ2.ПодотчетноеЛицо
            И ВТ1.Сумма = -ВТ2.Сумма
            И ВТ1.Номер < ВТ2.Номер

Важный момент: использование условия ВТ1.Номер < ВТ2.Номер необходимо для того, чтобы каждая пара строк попала в выборку только один раз, и строка не соединилась сама с собой (особенно если сумма равна нулю).

Метод 3: Оптимизация через объект Соответствие (Map)

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

Разберем алгоритм:

  1. Создаем Соответствие, где ключом будет сумма, а значением — массив строк с такой суммой.
  2. При обходе таблицы для текущей суммы проверяем, есть ли в Соответствии ключ с обратным знаком.
  3. Если есть — забираем одну строку из массива, пара сформирована.
  4. Если нет — добавляем текущую строку в массив по ключу её суммы.

Пары = Новый Массив;
Хранилище = Новый Соответствие; // Ключ: Сумма, Значение: Массив строк

Для Каждого СтрокаТЗ Из ТЗ Цикл
    ИскомаяСумма = -СтрокаТЗ.Сумма;
    СписокКандидатов = Хранилище.Получить(ИскомаяСумма);
    
    Если СписокКандидатов <> Неопределено И СписокКандидатов.Количество() > 0 Тогда
        // Нашли пару!
        ПараСтрок = Новый Структура("Плюс, Минус", СтрокаТЗ, СписокКандидатов[0]);
        Пары.Добавить(ПараСтрок);
        
        // Удаляем использованную строку из хранилища
        СписокКандидатов.Удалить(0);
    Иначе
        // Пары нет, запоминаем текущую строку для будущих поисков
        МойСписок = Хранилище.Получить(СтрокаТЗ.Сумма);
        Если МойСписок = Неопределено Тогда
            МойСписок = Новый Массив;
            Хранилище.Вставить(СтрокаТЗ.Сумма, МойСписок);
        КонецЕсли;
        МойСписок.Добавить(СтрокаТЗ);
    КонецЕсли;
КонецЦикла;

Этот метод работает со скоростью O(n), что делает его самым быстрым для больших объемов данных в памяти.

Метод 4: Сортировка и два указателя

Посмотрим на классический алгоритмический подход, который часто применяется в программировании. Если отсортировать таблицу по колонке Сумма, то отрицательные числа окажутся в начале, а положительные — в конце.

Алгоритм действий:

Нюансы и рекомендации

При реализации любого из методов стоит учитывать проблему «один ко многим». Например, если у вас есть одна строка с суммой 100 и две строки по -100, алгоритм должен четко понимать, какую именно строку он схлопывает. Обычно рекомендуется использовать принцип FIFO (First In, First Out) — схлопывать те строки, которые появились в таблице раньше по времени или имеют меньший номер строки.

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


Для Счетчик = -(ТЗ.Количество() - 1) По 0 Цикл
    Индекс = -Счетчик;
    ТекущаяСтрока = ТЗ[Индекс];
    // Логика удаления...
КонецЦикла;

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

← На главную