Перейти к содержимому

Рабочий календарь - график, праздники, расчёт срока

Считаем сроки с учётом выходных и праздников: хранение календаря, расчёт даты через несколько рабочих дней и понятный вывод покупателю.

Что нужно знать заранее

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

График работы точки выдачи и календарь праздников - это две разные вещи. График говорит, когда работает склад или пункт выдачи, а календарь - какие дни считаются рабочими для срока доставки.

Время отсечки приёма заказов сдвигает весь расчёт срока сразу на целые сутки. Заказ, оформленный вечером, уезжает следующим рабочим днём, и покупателю честнее сразу показать реальную дату, а не обещание «завтра».

Шаги

  1. Завести хранилище календаря: список праздников и правила обычных выходных дней недели.
  2. Написать одну общую функцию расчёта даты через нужное число рабочих дней.
  3. Учесть время отсечки заказа и часовой пояс покупателя уже при самом выводе.
  4. Показывать покупателю готовую дату словами, а не число рабочих дней в скобках.
  5. Проверить расчёт на длинных январских праздниках и на переносах рабочих суббот года.

Решение

Храним календарь данными:

// инфоблок «Календарь»: элемент на каждый особый день года
$holidays = \CIBlockElement::GetList([], ['IBLOCK_ID' => $calendarId, 'ACTIVE' => 'Y'],
false, false, ['ID', 'ACTIVE_FROM', 'PROPERTY_TYPE'])->Fetch();
// тип дня: выходной или перенесённый рабочий, остальное считается по неделе

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

Считаем дату через несколько рабочих дней:

function addWorkDays(\Bitrix\Main\Type\DateTime $from, int $days, array $calendar): \Bitrix\Main\Type\DateTime
{
$date = clone $from;
while ($days > 0) {
$date->add('+1 day');
$key = $date->format('Y-m-d');
$isWorkDay = $calendar[$key] ?? !in_array((int)$date->format('N'), [6, 7], true);
if ($isWorkDay) { $days--; } // выходные и праздники не уменьшают счётчик
}
return $date;
}

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

Учитываем время отсечки:

$now = new \Bitrix\Main\Type\DateTime();
$cutoff = (new \Bitrix\Main\Type\DateTime())->setTime(16, 0);
$start = $now > $cutoff ? $now->add('+1 day') : $now; // после отсечки считаем со следующего дня
$delivery = addWorkDays($start, 2, $calendar);

Отсечку задают настройкой, а не константой в коде. Склад меняет её на время распродаж, и правка настройки не должна превращаться в выкладку новой версии сайта.

Показываем дату понятно:

printf("Доставим %s\n", \Bitrix\Main\Type\Date::createFromTimestamp($delivery->getTimestamp())
->format('j F, D'));
// «Доставим 3 сентября, ср» понятнее, чем «срок 2 рабочих дня»

Выводим график работы на витрине:

$schedule = ['пн-пт' => '9:00-19:00', 'сб' => '10:00-16:00', 'вс' => 'выходной'];
foreach ($schedule as $days => $hours) { printf("%-6s %s\n", $days, $hours); }
// график хранят рядом с пунктом выдачи, а не в вёрстке шаблона

График работы держат рядом с точкой: у каждого пункта выдачи он свой. Общий график в шаблоне живёт ровно до открытия второго пункта с другими часами.

Типичные проблемы

Срок доставки попадает на праздники.

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

Вечерние заказы обещают доставку на завтра.

В расчёте не учтено время отсечки приёма заказов на самом складе магазина. Отсечку задают отдельной настройкой и сдвигают старт расчёта на следующий рабочий день.

Дата на витрине отличается от даты в письме.

Расчёт выполняется в двух разных местах проекта и совершенно разным кодом. Расчёт держат в одной общей функции проекта и зовут её из всех мест.

У покупателя из другого часового пояса дата на день раньше.

Время считается в часовом поясе сервера, без всякого учёта пояса самого покупателя. Пояс учитывают только при выводе, а хранят время в едином поясе проекта.

После переноса рабочей субботы расчёт неверен.

Календарь хранит только выходные дни и совсем не умеет помечать рабочие субботы. У особого дня календаря заводят свой тип: выходной или перенесённый рабочий день.

Частые вопросы

Где хранить календарь праздников?

В инфоблоке или своей таблице с типом дня. Главное, чтобы список правила редакция без разработчика: праздники меняются ежегодно.

Нужен ли отдельный календарь на каждый склад?

Если графики отличаются - да, и это обычная ситуация для сети пунктов выдачи. Общий календарь праздников при этом остаётся один.

Как показать срок в карточке товара?

Считать дату при выводе и кэшировать её на несколько часов. Пересчитывать на каждый показ незачем: дата меняется раз в сутки.

Что делать с часовыми поясами?

Хранить время в едином поясе проекта, а выводить в поясе посетителя. Смешение поясов даёт расхождение ровно на один день у дальних регионов.

Стоит ли обещать точную дату?

Да, но с оговоркой про службу доставки: покупателю понятнее дата, чем число дней. Обещание без оговорки превращается в жалобу при первой задержке.

Смежное

Первоисточник