В практике разработчика 1С часто возникает задача очистки данных или корректировки движений, когда в таблице значений (ТЗ) присутствуют записи, которые фактически аннулируют друг друга. Типичный пример: две строки по одному и тому же аналитическому разрезу (например, подотчетное лицо или подразделение), где суммы равны по модулю, но имеют разные знаки (например, 100 и -100). Такие строки необходимо найти и «схлопнуть», чтобы они не искажали итоговые обороты. Рассмотрим подробнее несколько способов решения этой задачи, от простых переборов до оптимизированных алгоритмов.
Проанализируем самый наглядный способ, который подходит для небольших и средних таблиц. Суть заключается в том, чтобы помечать уже «обработанные» строки, чтобы не использовать их повторно в других парах. Разберем этот процесс по шагам:
Обработано с типом Булево.Сумма.-ТекСтрока.Сумма.Посмотрим на пример кода, реализующего эту логику:
// Добавляем служебную колонку
ТЗ.Колонки.Добавить("Обработано", Новый ОписаниеТипов("Булево"));
ТЗ.Индексы.Добавить("Сумма");
Для Каждого СтрокаИзм Из ТЗ Цикл
Если СтрокаИзм.Обработано Тогда
Продолжить;
КонецЕсли;
// Параметры поиска: ищем противоположную сумму, которая еще не занята
ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("Сумма", -СтрокаИзм.Сумма);
ПараметрыОтбора.Вставить("Обработано", Ложь);
НайденныеСтроки = ТЗ.НайтиСтроки(ПараметрыОтбора);
Для Каждого Кандидат Из НайденныеСтроки Цикл
// Проверяем, что это не та же самая строка (актуально для нулевых сумм)
Если Кандидат = СтрокаИзм Тогда
Продолжить;
КонецЕсли;
// Если нашли подходящую пару, помечаем обе строки
СтрокаИзм.Обработано = Истина;
Кандидат.Обработано = Истина;
Прервать; // Переходим к следующей необработанной строке
КонецЦикла;
КонецЦикла;
После выполнения этого кода все строки, у которых Обработано равно Истина, можно либо удалить, либо использовать для формирования сторнирующих записей.
Выясним, как решить задачу наиболее эффективно, если данных очень много или они уже находятся в базе. Использование механизма запросов позволяет избежать медленных циклов 1С. Основная идея — поместить таблицу во временную таблицу и соединить её саму с собой по условию равенства сумм с обратным знаком.
Рассмотрим пример запроса:
ВЫБРАТЬ
ТЗ.Номер КАК Номер,
ТЗ.ПодотчетноеЛицо КАК ПодотчетноеЛицо,
ТЗ.Сумма КАК Сумма
ПОМЕСТИТЬ ВТ_Данные
ИЗ
&ВнешняяТЗ КАК ТЗ;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ1.Номер КАК НомерПлюс,
ВТ2.Номер КАК НомерМинус,
ВТ1.Сумма КАК Сумма
ИЗ
ВТ_Данные КАК ВТ1
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Данные КАК ВТ2
ПО ВТ1.ПодотчетноеЛицо = ВТ2.ПодотчетноеЛицо
И ВТ1.Сумма = -ВТ2.Сумма
И ВТ1.Номер < ВТ2.Номер
Важный момент: использование условия ВТ1.Номер < ВТ2.Номер необходимо для того, чтобы каждая пара строк попала в выборку только один раз, и строка не соединилась сама с собой (особенно если сумма равна нулю).
Если нам критична производительность (например, в таблице десятки тысяч строк), использование вложенных поисков через НайтиСтроки может быть медленным. Проанализируем ситуацию с использованием объекта Соответствие. Этот метод позволяет найти все пары за один проход по таблице.
Разберем алгоритм:
Соответствие, где ключом будет сумма, а значением — массив строк с такой суммой.Соответствии ключ с обратным знаком.
Пары = Новый Массив;
Хранилище = Новый Соответствие; // Ключ: Сумма, Значение: Массив строк
Для Каждого СтрокаТЗ Из ТЗ Цикл
ИскомаяСумма = -СтрокаТЗ.Сумма;
СписокКандидатов = Хранилище.Получить(ИскомаяСумма);
Если СписокКандидатов <> Неопределено И СписокКандидатов.Количество() > 0 Тогда
// Нашли пару!
ПараСтрок = Новый Структура("Плюс, Минус", СтрокаТЗ, СписокКандидатов[0]);
Пары.Добавить(ПараСтрок);
// Удаляем использованную строку из хранилища
СписокКандидатов.Удалить(0);
Иначе
// Пары нет, запоминаем текущую строку для будущих поисков
МойСписок = Хранилище.Получить(СтрокаТЗ.Сумма);
Если МойСписок = Неопределено Тогда
МойСписок = Новый Массив;
Хранилище.Вставить(СтрокаТЗ.Сумма, МойСписок);
КонецЕсли;
МойСписок.Добавить(СтрокаТЗ);
КонецЕсли;
КонецЦикла;
Этот метод работает со скоростью O(n), что делает его самым быстрым для больших объемов данных в памяти.
Посмотрим на классический алгоритмический подход, который часто применяется в программировании. Если отсортировать таблицу по колонке Сумма, то отрицательные числа окажутся в начале, а положительные — в конце.
Алгоритм действий:
ТЗ.Сортировать("Сумма Возр");.При реализации любого из методов стоит учитывать проблему «один ко многим». Например, если у вас есть одна строка с суммой 100 и две строки по -100, алгоритм должен четко понимать, какую именно строку он схлопывает. Обычно рекомендуется использовать принцип FIFO (First In, First Out) — схлопывать те строки, которые появились в таблице раньше по времени или имеют меньший номер строки.
Также помните о безопасности при удалении строк в цикле. Если вы решите удалять строки сразу после нахождения пары, всегда делайте обход с конца таблицы к началу. В противном случае индексы строк «поплывут», и вы получите ошибку или пропустите часть данных:
Для Счетчик = -(ТЗ.Количество() - 1) По 0 Цикл
Индекс = -Счетчик;
ТекущаяСтрока = ТЗ[Индекс];
// Логика удаления...
КонецЦикла;
Подводя итог, выбор метода зависит от ваших условий: для простых задач подойдет цикл с колонкой Обработано, для высоконагруженных систем — Соответствие, а если данные нужно предварительно фильтровать по сложным условиям — лучше всего использовать временные таблицы в запросах.