Как рассчитать количество дней до следующего заказа по графику доставки в 1С?

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

Рассмотрим распространенную прикладную ситуацию. В конфигурации 1С имеется непериодический независимый регистр сведений ГрафикДовоза. Регистр содержит два измерения — Поставщик и Магазин, а также семь булевых ресурсов, соответствующих дням недели: Пн, Вт, Ср, Чт, Пт, Сб, Вс. Когда в определенный день недели для магазина разрешен заказ, в соответствующем ресурсе устанавливается значение Истина.

Перед нами стоит задача: зная текущий день недели (число от 1 до 7, где 1 — понедельник, 7 — воскресенье), быстро и эффективно определить, через сколько дней наступит следующий день заказа относительно сегодняшнего дня. Изменять структуру регистра нельзя, так как на него завязана другая функциональность конфигурации.

Разберем различные подходы к решению этой задачи: с помощью языка запросов 1С и средствами встроенного языка.

Способ 1. Расчет в запросе через нормализацию дней и объединение таблиц

Проанализируем классический подход, предложенный на форуме. Поскольку ресурсы регистра представлены отдельными колонками, их необходимо привести к единому плоскому списку строк, сопоставив каждому ресурсу порядковый номер дня недели. Для этого используем конструкцию ОБЪЕДИНИТЬ ВСЕ, формируя временную таблицу.

Посмотрим на пример формирования временной таблицы с номерами дней:


ВЫБРАТЬ
    ГрафикДовоза.Поставщик КАК Поставщик,
    ГрафикДовоза.Магазин КАК Магазин,
    1 КАК День
ПОМЕСТИТЬ ВТ_Дни
ИЗ
    РегистрСведений.ГрафикДовоза КАК ГрафикДовоза
ГДЕ
    ГрафикДовоза.Пн = ИСТИНА

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ГрафикДовоза.Поставщик,
    ГрафикДовоза.Магазин,
    2
ИЗ
    РегистрСведений.ГрафикДовоза КАК ГрафикДовоза
ГДЕ
    ГрафикДовоза.Вт = ИСТИНА

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ГрафикДовоза.Поставщик,
    ГрафикДовоза.Магазин,
    3
ИЗ
    РегистрСведений.ГрафикДовоза КАК ГрафикДовоза
ГДЕ
    ГрафикДовоза.Ср = ИСТИНА

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ГрафикДовоза.Поставщик,
    ГрафикДовоза.Магазин,
    4
ИЗ
    РегистрСведений.ГрафикДовоза КАК ГрафикДовоза
ГДЕ
    ГрафикДовоза.Чт = ИСТИНА

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ГрафикДовоза.Поставщик,
    ГрафикДовоза.Магазин,
    5
ИЗ
    РегистрСведений.ГрафикДовоза КАК ГрафикДовоза
ГДЕ
    ГрафикДовоза.Пт = ИСТИНА

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ГрафикДовоза.Поставщик,
    ГрафикДовоза.Магазин,
    6
ИЗ
    РегистрСведений.ГрафикДовоза КАК ГрафикДовоза
ГДЕ
    ГрафикДовоза.Сб = ИСТИНА

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ГрафикДовоза.Поставщик,
    ГрафикДовоза.Магазин,
    7
ИЗ
    РегистрСведений.ГрафикДовоза КАК ГрафикДовоза
ГДЕ
    ГрафикДовоза.Вс = ИСТИНА
;

Теперь выясним, как рассчитать смещение. В языке запросов 1С отсутствует операция взятия остатка от деления, поэтому для учета перехода через границу недели (с воскресенья на понедельник) применим математическую конструкцию ВЫБОР КОГДА. Если день доставки строго больше текущего дня, мы просто вычитаем текущий день. Если день доставки меньше или равен текущему дню, этот день наступит только на следующей неделе, поэтому прибавляем 7 дней.

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


ВЫБРАТЬ
    ВТ_Дни.Поставщик КАК Поставщик,
    ВТ_Дни.Магазин КАК Магазин,
    МИНИМУМ(ВЫБОР
        КОГДА ВТ_Дни.День > &ТекущийДень
            ТОГДА ВТ_Дни.День - &ТекущийДень
        ИНАЧЕ ВТ_Дни.День + 7 - &ТекущийДень
    КОНЕЦ) КАК ДнейДоЗаказа
ИЗ
    ВТ_Дни КАК ВТ_Дни
СГРУППИРОВАТЬ ПО
    ВТ_Дни.Поставщик,
    ВТ_Дни.Магазин

Параметр &ТекущийДень можно передать из встроенного языка с помощью функции ДеньНедели(), либо вычислить прямо в запросе с помощью функции ДЕНЬНЕДЕЛИ(&ТекущаяДата).

Способ 2. Оптимальный расчет в запросе за один проход (без ОБЪЕДИНИТЬ ВСЕ)

Разберем тонкость производительности: конструкция с семью ОБЪЕДИНИТЬ ВСЕ заставляет СУБД сканировать таблицу регистра до 7 раз. Если записей много, это создает лишнюю нагрузку на дисковую подсистему. Мы можем получить результат за одно чтение таблицы с помощью проверки смещений от 1 до 7 дней в единой конструкции ВЫБОР КОГДА.

Рассмотрим структуру такого запроса. Сначала проверяем, есть ли доставка через 1 день, затем — через 2 дня, и так далее:


ВЫБРАТЬ
    График.Поставщик КАК Поставщик,
    График.Магазин КАК Магазин,
    ВЫБОР
        // Смещение +1 день
        КОГДА (&ТекущийДень = 1 И График.Вт)
           ИЛИ (&ТекущийДень = 2 И График.Ср)
           ИЛИ (&ТекущийДень = 3 И График.Чт)
           ИЛИ (&ТекущийДень = 4 И График.Пт)
           ИЛИ (&ТекущийДень = 5 И График.Сб)
           ИЛИ (&ТекущийДень = 6 И График.Вс)
           ИЛИ (&ТекущийДень = 7 И График.Пн) ТОГДА 1

        // Смещение +2 дня
        КОГДА (&ТекущийДень = 1 И График.Ср)
           ИЛИ (&ТекущийДень = 2 И График.Чт)
           ИЛИ (&ТекущийДень = 3 И График.Пт)
           ИЛИ (&ТекущийДень = 4 И График.Сб)
           ИЛИ (&ТекущийДень = 5 И График.Вс)
           ИЛИ (&ТекущийДень = 6 И График.Пн)
           ИЛИ (&ТекущийДень = 7 И График.Вт) ТОГДА 2

        // Смещение +3 дня
        КОГДА (&ТекущийДень = 1 И График.Чт)
           ИЛИ (&ТекущийДень = 2 И График.Пт)
           ИЛИ (&ТекущийДень = 3 И График.Сб)
           ИЛИ (&ТекущийДень = 4 И График.Вс)
           ИЛИ (&ТекущийДень = 5 И График.Пн)
           ИЛИ (&ТекущийДень = 6 И График.Вт)
           ИЛИ (&ТекущийДень = 7 И График.Ср) ТОГДА 3

        // Смещение +4 дня
        КОГДА (&ТекущийДень = 1 И График.Пт)
           ИЛИ (&ТекущийДень = 2 И График.Сб)
           ИЛИ (&ТекущийДень = 3 И График.Вс)
           ИЛИ (&ТекущийДень = 4 И График.Пн)
           ИЛИ (&ТекущийДень = 5 И График.Вт)
           ИЛИ (&ТекущийДень = 6 И График.Ср)
           ИЛИ (&ТекущийДень = 7 И График.Чт) ТОГДА 4

        // Смещение +5 дней
        КОГДА (&ТекущийДень = 1 И График.Сб)
           ИЛИ (&ТекущийДень = 2 И График.Вс)
           ИЛИ (&ТекущийДень = 3 И График.Пн)
           ИЛИ (&ТекущийДень = 4 И График.Вт)
           ИЛИ (&ТекущийДень = 5 И График.Ср)
           ИЛИ (&ТекущийДень = 6 И График.Чт)
           ИЛИ (&ТекущийДень = 7 И График.Пт) ТОГДА 5

        // Смещение +6 дней
        КОГДА (&ТекущийДень = 1 И График.Вс)
           ИЛИ (&ТекущийДень = 2 И График.Пн)
           ИЛИ (&ТекущийДень = 3 И График.Вт)
           ИЛИ (&ТекущийДень = 4 И График.Ср)
           ИЛИ (&ТекущийДень = 5 И График.Чт)
           ИЛИ (&ТекущийДень = 6 И График.Пт)
           ИЛИ (&ТекущийДень = 7 И График.Сб) ТОГДА 6

        // Смещение +7 дней (доставка только в тот же день недели)
        КОГДА (&ТекущийДень = 1 И График.Пн)
           ИЛИ (&ТекущийДень = 2 И График.Вт)
           ИЛИ (&ТекущийДень = 3 И График.Ср)
           ИЛИ (&ТекущийДень = 4 И График.Чт)
           ИЛИ (&ТекущийДень = 5 И График.Пт)
           ИЛИ (&ТекущийДень = 6 И График.Сб)
           ИЛИ (&ТекущийДень = 7 И График.Вс) ТОГДА 7

        ИНАЧЕ NULL
    КОНЕЦ КАК ДнейДоЗаказа
ИЗ
    РегистрСведений.ГрафикДовоза КАК График

Этот способ отрабатывает значительно быстрее, так как не создает временных таблиц и выполняет сканирование физической таблицы только один раз.

Способ 3. Вычисление на встроенном языке с оператором остатка от деления

Если данные регистра уже получены в выборку или структуру и расчет требуется для конкретной пары поставщик-магазин в коде формы или модуля, производительнее выполнить расчет во встроенном языке 1С. В отличие от языка запросов, во встроенном языке поддерживается оператор остатка от деления %.

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

  1. Помещаем булевы флаги недели в массив из 7 элементов, где индексы от 0 до 6 соответствуют дням с понедельника по воскресенье.
  2. Запускаем цикл со смещением от 1 до 7.
  3. Вычисляем индекс следующего дня по формуле: (ТекущийДеньНедели - 1 + Смещение) % 7.
  4. При первой же найденной Истина прерываем цикл — текущее смещение и есть искомое количество дней.

Посмотрим на пример функции:


Функция ПолучитьДнейДоСледующегоЗаказа(СтрокаГрафика, ТекущаяДата) Экспорт

    ТекущийДень = ДеньНедели(ТекущаяДата); // Возвращает число от 1 до 7
    
    // Заполняем массив дней (индексация от 0 до 6)
    ДниГрафика = Новый Массив(7);
    ДниГрафика[0] = СтрокаГрафика.Пн;
    ДниГрафика[1] = СтрокаГрафика.Вт;
    ДниГрафика[2] = СтрокаГрафика.Ср;
    ДниГрафика[3] = СтрокаГрафика.Чт;
    ДниГрафика[4] = СтрокаГрафика.Пт;
    ДниГрафика[5] = СтрокаГрафика.Сб;
    ДниГрафика[6] = СтрокаГрафика.Вс;
    
    Для Смещение = 1 По 7 Цикл
        ИндексДня = (ТекущийДень - 1 + Смещение) % 7;
        Если ДниГрафика[ИндексДня] Тогда
            Возврат Смещение;
        КонецЕсли;
    КонецЦикла;
    
    // Если ни один флаг не установлен
    Возврат Неопределено;

КонецФункции

Способ 4. Строковый алгоритм без циклов во встроенном языке

Еще один элегантный способ на встроенном языке — моделирование кольцевого буфера через сдвоенную строку. Преобразуем 7 флагов в строку из единиц и нулей, дублируем ее и ищем ближайшую единицу.


Функция ДнейДоЗаказаСтрокой(СтрокаГрафика, ТекущаяДата) Экспорт

    ТекущийДень = ДеньНедели(ТекущаяДата);
    
    // Формируем битовую маску недели длиной 7 символов
    Маска = ?(СтрокаГрафика.Пн, "1", "0")
          + ?(СтрокаГрафика.Вт, "1", "0")
          + ?(СтрокаГрафика.Ср, "1", "0")
          + ?(СтрокаГрафика.Чт, "1", "0")
          + ?(СтрокаГрафика.Пт, "1", "0")
          + ?(СтрокаГрафика.Сб, "1", "0")
          + ?(СтрокаГрафика.Вс, "1", "0");
          
    // Сдваиваем строку для перехода через границу недели
    ДвойнаяМаска = Маска + Маска;
    
    // Берем окно из 7 дней, начиная со следующего дня относительно текущего
    ОкноПоиска = Сред(ДвойнаяМаска, ТекущийДень + 1, 7);
    
    Позиция = Найти(ОкноПоиска, "1");
    Если Позиция > 0 Тогда
        Возврат Позиция; // Позиция символа "1" в окне точно равна смещению в днях
    КонецЕсли;
    
    Возврат Неопределено;

КонецФункции

Важные практические нюансы

При реализации данного алгоритма обязательно учитываем граничные случаи:

  1. Отсутствие расписания: если для пары контрагент-магазин все ресурсы содержат Ложь, алгоритм должен корректно возвращать NULL или Неопределено, предотвращая зацикливание или ложный заказ через 7 дней.
  2. Время заказа (дедлайн): если заказ формируется в текущий день (например, сегодня понедельник и флаг Пн активен), нужно проверять текущее время. Если время формирования заявки еще не истекло, заказ можно отправить сегодня со смещением в 0 дней. Если время вышло — смещение должно вести отсчет до следующего доступного дня недели.
← На главную