Как отфильтровать отрицательные числа и правильно удалить строки из таблицы значений в 1С

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

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

Разберем по шагам, как устроена логика математического сравнения в платформе 1С, почему стандартный цикл Для Каждого приводит к логическим сбоям при вызове метода Удалить(), и рассмотрим наиболее надежные и производительные способы решения этой задачи.

Определение знака числа и контроль типов данных

С математической точки зрения отрицательными являются строго те числа, которые меньше нуля (значение 0 не является ни положительным, ни отрицательным). Во встроенном языке 1С нет отдельной функции знака числа (вроде Sign), поэтому проверка осуществляется стандартным оператором сравнения: Значение < 0.

Однако при работе с реальными таблицами значений важно учитывать следующие особенности платформы:

  1. Проверка типов: Если колонка таблицы значений не имеет строгой типизации или содержит составной тип данных, в ячейке могут оказаться значения Неопределено, NULL или строки. Попытка выполнить операцию Значение < 0 для нечислового типа приведет к ошибке времени выполнения «Неверные параметры при сравнении». Перед сравнением необходимо убедиться, что тип значения является числом: ТипЗнч(Значение) = Тип("Число").
  2. Артефакты машинного округления: В результате сложных вычислений, деления или распределения сумм в числе может возникнуть бесконечно малый отрицательный остаток (например, -0.000000001). На форме документа такое число отобразится как 0,00, но условие Значение < 0 вернет значение Истина. Чтобы избежать ложного срабатывания, выполняем сравнение после вызова функции Окр(): Окр(Значение, 2) < 0.
  3. Ограничение метода НайтиСтроки(): Метод НайтиСтроки() принимает структуру отбора и работает исключительно по точному совпадению. Отобрать строки по условию неравенства или операторам больше/меньше с его помощью невозможно.

Почему нельзя удалять строки в стандартном цикле «Для Каждого»

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


Для Каждого СтрокаТЗ Из ТаблицаДанных Цикл
    Если Не СтрокаТЗ.Сумма < 0 Тогда
        ТаблицаДанных.Удалить(СтрокаТЗ);
    КонецЕсли;
КонецЦикла;

Такой код работает некорректно. Когда мы вызываем метод Удалить(), платформа немедленно удаляет строку из памяти, а все последующие строки сдвигаются на один индекс назад. Внутренний итератор цикла при этом переходит к следующему порядковому номеру. В результате строка, которая стояла сразу за удаленной, смещается на место удаленной и полностью перескакивает проверку. Если в таблице идут две ненужные строки подряд, вторая гарантированно останется в таблице.

Чтобы обойти эту проблему, применяются три проверенных подхода.

Способ 1. Обход таблицы с конца по индексу (обратный цикл)

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

Посмотрим на пример реализации через цикл Для:


Граница = ТаблицаДанных.Количество() - 1;

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

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

Способ 2. Двухпроходный сбор ссылок на строки в массив

В современной разработке на платформе 1С наиболее читаемым и общепринятым считается подход с разделением операций поиска и удаления. Сначала мы обходим таблицу простым циклом Для Каждого, находим все строки, которые не удовлетворяют условию, и складываем ссылки на них в массив. Затем вторым циклом обходим полученный массив и удаляем элементы из исходной таблицы.

Разберем по шагам программный код:


СтрокиДляУдаления = Новый Массив;

// Шаг 1. Отбор строк, не являющихся строго отрицательными
Для Каждого СтрокаТЗ Из ТаблицаДанных Цикл
    Если ТипЗнч(СтрокаТЗ.Сумма) = Тип("Число") Тогда
        // Оставляем только строго отрицательные, остальные помечаем на удаление
        Если СтрокаТЗ.Сумма >= 0 Тогда
            СтрокиДляУдаления.Добавить(СтрокаТЗ);
        КонецЕсли;
    Иначе
        СтрокиДляУдаления.Добавить(СтрокаТЗ);
    КонецЕсли;
КонецЦикла;

// Шаг 2. Удаление собранных строк
Для Каждого УдаляемаяСтрока Из СтрокиДляУдаления Цикл
    ТаблицаДанных.Удалить(УдаляемаяСтрока);
КонецЦикла;

Главное достоинство этого метода — максимальная наглядность и простота отладки. Кроме того, метод ТаблицаЗначений.Удалить() может принимать в качестве параметра не только числовой индекс, но и саму ссылку на объект СтрокаТаблицыЗначений.

Способ 3. Очистка через метод «Скопировать»

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


ОтрицательныеСтроки = Новый Массив;

Для Каждого СтрокаТЗ Из ТаблицаДанных Цикл
    Если ТипЗнч(СтрокаТЗ.Сумма) = Тип("Число") И СтрокаТЗ.Сумма < 0 Тогда
        ОтрицательныеСтроки.Добавить(СтрокаТЗ);
    КонецЕсли;
КонецЦикла;

// Получаем таблицу, содержащую только нужные строки
ТаблицаДанных = ТаблицаДанных.Скопировать(ОтрицательныеСтроки);

Способ 4. Фильтрация с помощью объекта «Запрос»

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

Проанализируем реализацию через объект Запрос:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ТЗ.Номенклатура КАК Номенклатура,
    |    ТЗ.Сумма КАК Сумма
    |ПОМЕСТИТЬ ВТ_Исходная
    |ИЗ
    |    &ТаблицаДанных КАК ТЗ
    |;
    |
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    ВТ.Номенклатура КАК Номенклатура,
    |    ВТ.Сумма КАК Сумма
    |ИЗ
    |    ВТ_Исходная КАК ВТ
    |ГДЕ
    |    ВТ.Сумма < 0";

Запрос.УстановитьПараметр("ТаблицаДанных", ТаблицаДанных);
ТаблицаДанных = Запрос.Выполнить().Выгрузить();

Использование запроса исключает ошибки пропуска строк, автоматически обрабатывает типы данных на уровне условий ГДЕ и обеспечивает максимальное быстродействие при обработке крупных массивов информации.

← На главную