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

Программист 1С v7.7 Управленческий учет
← На главную

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

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

Выясняем причины ошибок в исходном коде

Проанализируем фрагмент кода, представленный автором темы в самом начале обсуждения:


Если Таб.ДатаРаботы=Таб.ДатаРаботы тогда //сравнивать по этому значению строки
    Коллл=Коллл+1;
КонецЕсли;

Такой подход не работает по двум очевидным причинам:

  1. Условие Таб.ДатаРаботы = Таб.ДатаРаботы является тавтологией. Мы сравниваем значение колонки текущей строки с самим собой. Это условие всегда будет возвращать истину (1), и счетчик увеличится на каждой строке, игнорируя дубли дат.
  2. В платформе 1С 7.7 при обходе таблицы значений в цикле Пока Таб.ПолучитьСтроку() = 1 Цикл у нас нет встроенного прямого доступа к "следующей" или "предыдущей" строке для сравнения дат между собой без дополнительного позиционирования или сохранения значений в промежуточные переменные.

Кроме того, в коде отбора присутствует потенциальная архитектурная ошибка сравнения типов данных:


Если Таб.ФИО<>Водитель Тогда 
    Продолжить; 
КонецЕсли;

Если колонка ФИО в вашей таблице значений заполнена строковым представлением (например, получена методом Наименование или напрямую из текстового реквизита), а переменная Водитель является ссылкой на элемент справочника Сотрудники, то условие сравнения всегда будет возвращать истину (они не равны). Для корректного сопоставления необходимо сравнивать однотипные данные, например, строки: Таб.ФИО <> Водитель.Наименование, либо хранить в колонке ТЗ ссылки на элементы справочника.

Давайте подробно рассмотрим три надежных способа решения задачи подсчета уникальных дней работы.

Способ 1. Двойное свертывание временной таблицы (Классический метод)

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

Разберем по шагам алгоритм двойной свертки:

  1. Создаем вспомогательную таблицу значений и копируем в нее структуру и данные из основной таблицы.
  2. Делаем первую свертку по колонкам ФИО, Марка и ДатаРаботы. Это действие моментально удалит все дубликаты строк, где один и тот же водитель работал на одной машине в один и тот же день.
  3. Добавляем числовую колонку-счетчик (например, КоличествоДней) и заполняем каждую оставшуюся строку единицей.
  4. Делаем вторую свертку по колонкам ФИО и Марка, указывая колонку КоличествоДней в качестве суммируемой. На этом этапе даты "схлопнутся", а единицы сложатся. Мы получим чистые итоги отработанных дней в разрезе аналитики.
  5. В цикле обхода применяем фильтры пользователя и получаем искомый результат.

Посмотрим на практическую реализацию этого алгоритма:


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

// Шаг 2. Первая свертка для удаления дублей внутри одного дня
// Второй параметр пустой, так как числовых колонок для суммирования пока нет
ТабДни.Свернуть("ФИО, Марка, ДатаРаботы", "");

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

// Шаг 4. Вторая свертка для суммирования дней по группировкам
ТабДни.Свернуть("ФИО, Марка", "КоличествоДней");

// Шаг 5. Обход свернутой таблицы и применение пользовательских фильтров
Коллл = 0; // Наш итоговый счетчик дней
ТабДни.ВыбратьСтроки(); 
Пока ТабДни.ПолучитьСтроку() = 1 Цикл 
    
    // Проверяем фильтр по водителю
    Если ПустоеЗначение(Водитель) = 0 Тогда 
        // Приводим типы к строкам во избежание ошибок сравнения справочник-строка
        Если СокрЛП(ТабДни.ФИО) <> СокрЛП(Водитель.Наименование) Тогда 
            Продолжить; 
        КонецЕсли; 
    КонецЕсли; 
    
    // Проверяем фильтр по марке автомобиля
    Если ПустоеЗначение(Марка) = 0 Тогда 
        Если ТабДни.Марка <> Марка Тогда 
            Продолжить; 
        КонецЕсли; 
    КонецЕсли; 
    
    // Если строка прошла все фильтры, получаем количество дней
    Коллл = ТабДни.КоличествоДней;
    Прервать; // Прерываем цикл, если нам нужен был показатель по конкретному водителю
КонецЦикла;

Способ 2. Оптимизированный подход (Фильтрация ДО свертки)

Рассмотренный выше метод отлично работает, но имеет один недостаток с точки зрения производительности. Если исходная таблица содержит десятки тысяч строк (например, путевые листы крупного автопарка за год), то копирование и двойная свертка всей таблицы потребуют значительных ресурсов процессора и оперативной памяти.

Проанализируем ситуацию: зачем нам сворачивать данные по всем сотрудникам, если пользователь выбрал в отчете конкретного водителя и конкретную марку машины? Логичнее сначала отфильтровать строки, перенеся только подходящие во вспомогательную ТЗ, а уже потом сворачивать ее.

Давайте посмотрим на оптимизированный вариант кода:


// Создаем пустую таблицу нужной структуры для хранения отфильтрованных данных
ТабФильтр = СоздатьОбъект("ТаблицаЗначений");
ТабФильтр.НоваяКолонка("ДатаРаботы");

// Обходим исходную ТЗ и копируем только строки, прошедшие отбор
Таб.ВыбратьСтроки();
Пока Таб.ПолучитьСтроку() = 1 Цикл
    
    // Фильтр по Водителю
    Если ПустоеЗначение(Водитель) = 0 Тогда
        Если СокрЛП(Таб.ФИО) <> СокрЛП(Водитель.Наименование) Тогда
            Продолжить;
        КонецЕсли;
    КонецЕсли;
    
    // Фильтр по Марке
    Если ПустоеЗначение(Марка) = 0 Тогда
        Если Таб.Марка <> Марка Тогда
            Продолжить;
        КонецЕсли;
    КонецЕсли;
    
    // Строка подходит под критерии отбора. Добавляем дату во временную таблицу
    ТабФильтр.НоваяСтрока();
    ТабФильтр.ДатаРаботы = Таб.ДатаРаботы;
КонецЦикла;

// Теперь сворачиваем только отфильтрованные даты
ТабФильтр.Свернуть("ДатаРаботы", "");

// Количество строк в свернутой таблице и есть количество уникальных дней!
КоличествоДней = ТабФильтр.КоличествоСтрок();

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

Способ 3. Использование объекта СписокЗначений (Самый изящный вариант)

Можно ли решить задачу вообще без создания вспомогательных таблиц значений? Да, для этого идеально подходит системный объект СписокЗначений. Метод списка значений НайтиЗначение позволяет быстро проверять уникальность добавляемых элементов.

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

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

Рассмотрим пример кода:


СписокДней = СоздатьОбъект("СписокЗначений");

Таб.ВыбратьСтроки();
Пока Таб.ПолучитьСтроку() = 1 Цикл
    
    // 1. Применяем отбор по Водителю
    Если ПустоеЗначение(Водитель) = 0 Тогда
        Если СокрЛП(Таб.ФИО) <> СокрЛП(Водитель.Наименование) Тогда
            Продолжить;
        КонецЕсли;
    КонецЕсли;
    
    // 2. Применяем отбор по Марке автомобиля
    Если ПустоеЗначение(Марка) = 0 Тогда
        Если Таб.Марка <> Марка Тогда
            Продолжить;
        КонецЕсли;
    КонецЕсли;
    
    // 3. Проверяем дату работы на уникальность
    // Если такой даты еще нет в нашем списке, добавляем её
    Если СписокДней.НайтиЗначение(Таб.ДатаРаботы) = 0 Тогда
        СписокДней.ДобавитьЗначение(Таб.ДатаРаботы);
    КонецЕсли;
    
КонецЦикла;

// Итог: количество уникальных рабочих дней
КоличествоДней = СписокДней.РазмерСписка();

Преимущества использования списка значений:

Важные рекомендации по работе с типами данных в 1С 7.7

При реализации любого из предложенных алгоритмов всегда обращайте внимание на типы сравниваемых данных. На платформе 1С 7.7 типизация не строгая, но попытка сравнить объект типа СправочникСсылка со строковым значением Строка без явного преобразования не вызовет ошибку компиляции, но всегда будет возвращать Ложь в условиях равенства.

Если в вашей таблице значений колонка ФИО содержит текстовые данные, а отбор на форме отчета Водитель является элементом справочника, всегда используйте конструкцию Водитель.Наименование для получения строки. Дополнительное применение функции СокрЛП() гарантирует, что пробелы в начале или конце наименования не исказят результаты фильтрации.

← На главную