При разработке отчетов и обработок на платформе 1С:Предприятие 7.7 программисты часто сталкиваются с необходимостью постобработки данных во временных таблицах. Типичный сценарий: данные из документов за период выгружаются в объект ТаблицаЗначений, затем пользователь накладывает интерактивные фильтры (например, по конкретному водителю или марке автомобиля), и система должна корректно рассчитать количество фактически отработанных дней.
Сложность заключается в том, что в исходных документах за один и тот же день по одному сотруднику может быть введено несколько строк (например, разные рейсы, разные виды работ или перевозка различных грузов). Если просто посчитать количество строк после фильтрации, мы получим завышенный результат. Нам же необходимо посчитать каждую уникальную дату работы сотрудника за один день, независимо от количества выполненных им операций за эти сутки. Разберем подробно, как решить эту задачу несколькими способами.
Проанализируем фрагмент кода, представленный автором темы в самом начале обсуждения:
Если Таб.ДатаРаботы=Таб.ДатаРаботы тогда //сравнивать по этому значению строки
Коллл=Коллл+1;
КонецЕсли;
Такой подход не работает по двум очевидным причинам:
Таб.ДатаРаботы = Таб.ДатаРаботы является тавтологией. Мы сравниваем значение колонки текущей строки с самим собой. Это условие всегда будет возвращать истину (1), и счетчик увеличится на каждой строке, игнорируя дубли дат.Пока Таб.ПолучитьСтроку() = 1 Цикл у нас нет встроенного прямого доступа к "следующей" или "предыдущей" строке для сравнения дат между собой без дополнительного позиционирования или сохранения значений в промежуточные переменные.Кроме того, в коде отбора присутствует потенциальная архитектурная ошибка сравнения типов данных:
Если Таб.ФИО<>Водитель Тогда
Продолжить;
КонецЕсли;
Если колонка ФИО в вашей таблице значений заполнена строковым представлением (например, получена методом Наименование или напрямую из текстового реквизита), а переменная Водитель является ссылкой на элемент справочника Сотрудники, то условие сравнения всегда будет возвращать истину (они не равны). Для корректного сопоставления необходимо сравнивать однотипные данные, например, строки: Таб.ФИО <> Водитель.Наименование, либо хранить в колонке ТЗ ссылки на элементы справочника.
Давайте подробно рассмотрим три надежных способа решения задачи подсчета уникальных дней работы.
Этот способ основан на использовании стандартного и очень быстрого метода Свернуть объекта ТаблицаЗначений. Чтобы не испортить исходные данные, которые могут понадобиться для дальнейшего вывода в печатную форму, мы будем производить все манипуляции над копией таблицы.
Разберем по шагам алгоритм двойной свертки:
ФИО, Марка и ДатаРаботы. Это действие моментально удалит все дубликаты строк, где один и тот же водитель работал на одной машине в один и тот же день.КоличествоДней) и заполняем каждую оставшуюся строку единицей.ФИО и Марка, указывая колонку КоличествоДней в качестве суммируемой. На этом этапе даты "схлопнутся", а единицы сложатся. Мы получим чистые итоги отработанных дней в разрезе аналитики.Посмотрим на практическую реализацию этого алгоритма:
// Шаг 1. Создаем копию таблицы с необходимыми колонками
ТабДни = СоздатьОбъект("ТаблицаЗначений");
Таб.Выгрузить(ТабДни,,,"ФИО, Марка, ДатаРаботы");
// Шаг 2. Первая свертка для удаления дублей внутри одного дня
// Второй параметр пустой, так как числовых колонок для суммирования пока нет
ТабДни.Свернуть("ФИО, Марка, ДатаРаботы", "");
// Шаг 3. Добавляем колонку-счетчик и заполняем единицами
ТабДни.НоваяКолонка("КоличествоДней", "Число");
ТабДни.ВыбратьСтроки();
Пока ТабДни.ПолучитьСтроку() = 1 Цикл
ТабДни.КоличествоДней = 1;
КонецЦикла;
// Шаг 4. Вторая свертка для суммирования дней по группировкам
ТабДни.Свернуть("ФИО, Марка", "КоличествоДней");
// Шаг 5. Обход свернутой таблицы и применение пользовательских фильтров
Коллл = 0; // Наш итоговый счетчик дней
ТабДни.ВыбратьСтроки();
Пока ТабДни.ПолучитьСтроку() = 1 Цикл
// Проверяем фильтр по водителю
Если ПустоеЗначение(Водитель) = 0 Тогда
// Приводим типы к строкам во избежание ошибок сравнения справочник-строка
Если СокрЛП(ТабДни.ФИО) <> СокрЛП(Водитель.Наименование) Тогда
Продолжить;
КонецЕсли;
КонецЕсли;
// Проверяем фильтр по марке автомобиля
Если ПустоеЗначение(Марка) = 0 Тогда
Если ТабДни.Марка <> Марка Тогда
Продолжить;
КонецЕсли;
КонецЕсли;
// Если строка прошла все фильтры, получаем количество дней
Коллл = ТабДни.КоличествоДней;
Прервать; // Прерываем цикл, если нам нужен был показатель по конкретному водителю
КонецЦикла;
Рассмотренный выше метод отлично работает, но имеет один недостаток с точки зрения производительности. Если исходная таблица содержит десятки тысяч строк (например, путевые листы крупного автопарка за год), то копирование и двойная свертка всей таблицы потребуют значительных ресурсов процессора и оперативной памяти.
Проанализируем ситуацию: зачем нам сворачивать данные по всем сотрудникам, если пользователь выбрал в отчете конкретного водителя и конкретную марку машины? Логичнее сначала отфильтровать строки, перенеся только подходящие во вспомогательную ТЗ, а уже потом сворачивать ее.
Давайте посмотрим на оптимизированный вариант кода:
// Создаем пустую таблицу нужной структуры для хранения отфильтрованных данных
ТабФильтр = СоздатьОбъект("ТаблицаЗначений");
ТабФильтр.НоваяКолонка("ДатаРаботы");
// Обходим исходную ТЗ и копируем только строки, прошедшие отбор
Таб.ВыбратьСтроки();
Пока Таб.ПолучитьСтроку() = 1 Цикл
// Фильтр по Водителю
Если ПустоеЗначение(Водитель) = 0 Тогда
Если СокрЛП(Таб.ФИО) <> СокрЛП(Водитель.Наименование) Тогда
Продолжить;
КонецЕсли;
КонецЕсли;
// Фильтр по Марке
Если ПустоеЗначение(Марка) = 0 Тогда
Если Таб.Марка <> Марка Тогда
Продолжить;
КонецЕсли;
КонецЕсли;
// Строка подходит под критерии отбора. Добавляем дату во временную таблицу
ТабФильтр.НоваяСтрока();
ТабФильтр.ДатаРаботы = Таб.ДатаРаботы;
КонецЦикла;
// Теперь сворачиваем только отфильтрованные даты
ТабФильтр.Свернуть("ДатаРаботы", "");
// Количество строк в свернутой таблице и есть количество уникальных дней!
КоличествоДней = ТабФильтр.КоличествоСтрок();
Этот способ работает значительно быстрее на больших массивах данных, так как мы полностью избавились от двойного вызова метода Свернуть и промежуточного заполнения ТЗ единицами в цикле. Фильтрация выполняется на лету.
Можно ли решить задачу вообще без создания вспомогательных таблиц значений? Да, для этого идеально подходит системный объект СписокЗначений. Метод списка значений НайтиЗначение позволяет быстро проверять уникальность добавляемых элементов.
Разберем по шагам этот элегантный метод:
СписокЗначений.Продолжить.НайтиЗначение(ДатаРаботы) возвращает 0 (значение не найдено), мы добавляем эту дату в список.РазмерСписка()) будет равно точному числу уникальных дней работы.Рассмотрим пример кода:
СписокДней = СоздатьОбъект("СписокЗначений");
Таб.ВыбратьСтроки();
Пока Таб.ПолучитьСтроку() = 1 Цикл
// 1. Применяем отбор по Водителю
Если ПустоеЗначение(Водитель) = 0 Тогда
Если СокрЛП(Таб.ФИО) <> СокрЛП(Водитель.Наименование) Тогда
Продолжить;
КонецЕсли;
КонецЕсли;
// 2. Применяем отбор по Марке автомобиля
Если ПустоеЗначение(Марка) = 0 Тогда
Если Таб.Марка <> Марка Тогда
Продолжить;
КонецЕсли;
КонецЕсли;
// 3. Проверяем дату работы на уникальность
// Если такой даты еще нет в нашем списке, добавляем её
Если СписокДней.НайтиЗначение(Таб.ДатаРаботы) = 0 Тогда
СписокДней.ДобавитьЗначение(Таб.ДатаРаботы);
КонецЕсли;
КонецЦикла;
// Итог: количество уникальных рабочих дней
КоличествоДней = СписокДней.РазмерСписка();
Преимущества использования списка значений:
При реализации любого из предложенных алгоритмов всегда обращайте внимание на типы сравниваемых данных. На платформе 1С 7.7 типизация не строгая, но попытка сравнить объект типа СправочникСсылка со строковым значением Строка без явного преобразования не вызовет ошибку компиляции, но всегда будет возвращать Ложь в условиях равенства.
Если в вашей таблице значений колонка ФИО содержит текстовые данные, а отбор на форме отчета Водитель является элементом справочника, всегда используйте конструкцию Водитель.Наименование для получения строки. Дополнительное применение функции СокрЛП() гарантирует, что пробелы в начале или конце наименования не исказят результаты фильтрации.