Учитывать непраздничные дни недели

У меня есть работающий проект Symfony, который включает в себя класс Holiday с несколькими сгруппированными статическими функциями для подсчета непраздничных дней недели. Сердце кода было взято из нескольких ответов в эта тема.

Я использую статические методы во всем своем коде, включая расширения веток, отчеты, создание диаграмм и т. д.

Самым большим недостатком этого является то, что мне периодически приходится заходить в эти методы и вручную вводить праздники на предстоящий год (да, я знаю, что мог бы сделать несколько лет сразу), но есть случайные «праздники», которые я добавляю для такие вещи, как выходные из-за плохой погоды. Добавление праздника вручную и отправка кода в мой репозиторий git просто кажется «неправильным» или, по крайней мере, не элегантным.

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

Так какое решение??

Вот мой класс Holiday, хотя приведенный здесь код отлично подходит для моих нужд. Я ищу элегантный способ переместить массив $holidays в базу данных.

<?php
namespace App\Controller;

use DateInterval;
use DatePeriod;
use DateTime;
use Exception;

class Holiday
{
    private static $workingDays = [1, 2, 3, 4, 5]; # date format = N (1 = Monday, ...)

    /**
     * Is submitted date a holiday?
     *
     * @param DateTime $date
     * @return bool
     */
    public static function isHoliday(DateTime $date): bool
    {
        $holidays = self::getHolidays();

        return
            in_array($date->format('Y-m-d'), $holidays) ||
            in_array($date->format('*-m-d'), $holidays);
    }

    public static function isWorkDay(DateTime $date): bool
    {
        if (!in_array($date->format('N'), self::$workingDays)) {
            return false;
        }
        if (self::isHoliday($date)) {
            return false;
        }

        return true;
    }

    /**
     * Count number of weekdays within a given date span, excluding holidays
     *
     * @param DateTime $from
     * @param DateTime $to
     * @return int
     * @throws Exception
     */
    public static function countWeekDays(DateTime $from, DateTime $to = null)
    {
        if (is_null($to)) {
            return null;
        }

        // from stackoverflow:
        // http://stackoverflow.com/questions/336127/calculate-business-days#19221403

        $from = clone($from);
        $from->setTime(0, 0, 0);
        $to = clone($to);
        $to->setTime(0, 0, 0);
        $interval = new DateInterval('P1D');
        $to->add($interval);
        $period = new DatePeriod($from, $interval, $to);

        $days = 0;
        /** @var DateTime $date */
        foreach ($period as $date) {
            if (self::isWorkDay($date)) {
                $days++;
            }
        }

        return $days;
    }

    /**
     * Return count of weekdays in given month
     *
     * @param int $month
     * @param int $year
     * @return int
     * @throws Exception
     */
    public static function countWeekDaysInMonthToDate(int $month, int $year): int
    {
        $d1 = new DateTime($year . '-' . $month . '-01');
        $d2 = new DateTime($d1->format('Y-m-t'));
        $today = new DateTime();

        return self::countWeekDays($d1, min($d2, $today));
    }


    /**
     * Returns an array of strings representing holidays in format 'Y-m-d'
     *
     * @return array
     */
    private static function getHolidays(): array
    {
        // TODO: Move holidays to database (?)

        $holidays = ['*-12-25', '*-01-01', '*-07-04',
            '2017-04-14', # Good Friday
            '2017-05-29', # Memorial day
            '2017-08-28', # Hurricane Harvey closure
            '2017-08-29', # Hurricane Harvey closure
            '2017-08-30', # Hurricane Harvey closure
            '2017-08-31', # Hurricane Harvey closure
            '2017-09-01', # Hurricane Harvey closure
            '2017-09-04', # Labor day
            '2017-11-23', # Thanksgiving
            '2017-11-24', # Thanksgiving

            #'2018-03-30', # Good Friday
            '2018-05-28', # Memorial day
            '2018-09-03', # Labor day
            '2018-11-22', # Thanksgiving
            '2018-11-23', # Thanksgiving
            '2018-12-24', # Christmas Eve
            '2018-12-31', # New Year's Eve

            '2019-04-19', # Good Friday
            '2019-05-27', # Memorial day
            '2019-09-02', # Labor day
            '2019-11-28', # Thanksgiving
            '2019-11-29', # Thanksgiving
        ]; # variable and fixed holidays

        return $holidays;
    }
}

Вы захотите использовать относительные даты для своих праздников, таких как fourth Thursday of November или fourth Thurdsay of November + 1 day, и изменить объект DateTimeImmutale, указав год, который нужно изменить. Например: 3v4l.org/pra1L Я создал аналогичный сервис, чтобы делать то же самое, но я связан соглашением о неразглашении, иначе я бы помог больше.

Will B. 29.05.2019 04:23

Спасибо @fyrye, но это не помогает в случайные дни, например, когда закрыто из-за плохой погоды, поэтому гибкость базы данных теряется.

ehymel 29.05.2019 05:29

Этого следовало ожидать, поскольку закрытие организации технически не является наблюдаемым праздником и не может быть определено статически. Но к нему легко обращаться, и его можно определить без базы данных, например, чтением CSV или var_export массива значений $_POST во включаемом файле. например: $holidays = include '/path/to/file.php'; и file.php содержит return array('....');

Will B. 29.05.2019 05:39
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
0
3
126
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В соответствии с моим предложением использовать относительные форматы для наблюдаемых праздников. Я добавил возможность использовать callables и периоды интервалов дат для более сложных дат. Я также включил пасху как праздник в качестве варианта использования, используя функцию easter_date из расширения PHP calendar.

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

Пример: https://3v4l.org/fnj67

(Демонстрирует использование файла include)

class Holiday
{            

    /**
     * holidays in [YYYY => [YYYY-MM-DD]] format
     * array|string[][]
     */
    private static $observableHolidays = [];

    //....

    /**
     * Is submitted date a holiday?
     *
     * @param DateTime $date
     * @return bool
     */
    public static function isHoliday(\DateTimeInterface $date): bool
    {
        $holidays = self::getHolidays($date);

        return \array_key_exists($date->format('Y-m-d'), array_flip($holidays[$date->format('Y')]));
    }

    /**
     * @see https://www.php.net/manual/en/function.easter-date.php
     * @param \DateTimeInterface $date
     * @return \DateTimeImmutable
     */
    private static function getEaster(\DateTimeInterface $date): \DateTimeImmutable
    {
        return (new \DateTimeImmutable())->setTimestamp(\easter_date($date->format('Y')));
    }

    /**
     * Returns an array of strings representing holidays in format 'Y-m-d'
     *
     * @return array|string[][]
     */
    public static function getHolidays(\DateTimeInterface $start): array
    {
        //static prevents redeclaring the variable
        static $relativeHolidays = [
            'new years day' => 'first day of January',
            'easter' => [__CLASS__, 'getEaster'],
            'memorial day' => 'second Monday of May',
            'independence day' => 'July 4th',
            'labor day' => 'first Monday of September',
            'thanksgiving' => 'fourth Thursday of November',
            'black friday' => 'fourth Thursday of November + 1 day',
            'christmas' => 'December 25th',
            'new years eve' => 'last day of December',

            //... add others like Veterans Day, MLK, Columbus Day, etc
        ];
        if (!$start instanceof \DateTimeImmutable) {
            //force using DateTimeImmutable
            $start = \DateTimeImmutable::createFromMutable($start);
        }
        //build the holidays to the specified year
        $start = $start->modify('first day of this year');

        //always generate an entire years worth of holidays
        $period = new \DatePeriod($start, new \DateInterval('P1Y'), 0);
        foreach ($period as $date) {
            $year = $date->format('Y');
            if (array_key_exists($year, self::$observableHolidays)) {
                continue;
            }
            self::$observableHolidays[$year] = [];
            foreach (self::$relativeHolidays as $relativeHoliday) {
                 if (\is_callable($relativeHoliday)) {
                    $holidayDate = $relativeHoliday($date);
                } elseif (0 === \strpos($relativeHoliday, 'P')) {
                    $holidayDate = $date->add(new \DateInterval($relativeHoliday));
                } else {
                    $holidayDate = $date->modify($relativeHoliday);
                }
                self::$observableHolidays[$year][] = $holidayDate->format('Y-m-d');
            }
        }

        return self::$observableHolidays;
    }
}

$holidays = Holiday::getHolidays(new \DateTime('2017-08-28'));

Результаты:

array (
  2017 => 
  array (
    0 => '2017-01-01',
    1 => '2017-04-16',
    2 => '2017-05-08',
    3 => '2017-07-04',
    4 => '2017-09-04',
    5 => '2017-11-23',
    6 => '2017-11-24',
    7 => '2017-12-25',
    8 => '2017-12-31',
  ),
)

Конкретные даты закрытия для вашей организации, которые не могут быть сохранены в относительном формате, желательно хранить в СУБД, такой как MySQL или SQLite. Которые затем можно получить после стандартных наблюдаемых праздников, когда встречаются эти годы.

Однако при отсутствии базы данных вы можете использовать включаемый файл, как это делает Symfony в своих объявлениях службы контейнеров, что также выигрывает от использования OPcache. В качестве альтернативы вы можете использовать компонент Сериализатор Symfony для создания, загрузки и сохранения не относительных данных в желаемом формате (JSON, CSV, XML, YAML).

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

class Holiday
{
    public const CLOSURES_FILE = '/tmp/closures.php';

    //...

    public static function getHolidays(\DateTimeInterface $date): array
    {
        //...

        $holidays = self::$observableHolidays;
        if (\is_file(self::CLOSURES_FILE)) {
           foreach (include self::CLOSURES_FILE as $year => $dates) {
                if (!\array_key_exists($year, $holidays)) {
                    $holidays[$year] = [];
                }
                $holidays[$year] = array_merge($holidays[$year], $dates);
            }
        }

        return $holidays;
    }
}

$date = new \DateTime('2017-08-28');
var_export(Holiday::getHolidays($date));
var_dump(Holiday::isHoliday($date));

Результаты:

array (
  2017 => 
  array (
    0 => '2017-01-01',
    1 => '2017-04-16',
    2 => '2017-05-08',
    3 => '2017-07-04',
    4 => '2017-09-04',
    5 => '2017-11-23',
    6 => '2017-11-24',
    7 => '2017-12-25',
    8 => '2017-12-31',
    9 => '2017-08-28',
    10 => '2017-08-29',
    11 => '2017-08-30',
    12 => '2017-08-31',
    13 => '2017-09-01',
  ),
)
bool(true)

Чтобы сохранить словарь включаемого файла, вы можете загружать и сохранять эти значения в своем приложении так, как вам нравится, например, с помощью простой формы значений $_POST.

$dictionary_array = ['year' => ['date1', 'date2']];
file_put_contents(Holiday::CLOSURES_FILE, '<?php return ' . var_export($dictionary_array) . ';');

Поскольку ваше приложение контролируется версиями, используйте или задайте путь к нужному файлу словаря, который игнорируется в вашем файле .gitignore, например var/holidays/holiday_dates.php.


В моем проекте Symfony я добавляю относительные и неотносительные значения даты известный в качестве параметров из конфигураций в свой сервис Holiday, например 2001-09-11. Затем я использую службу запросов для ввода неотносительных дат неизвестный из базы данных в класс Holiday, таких как закрытие ваших ураганов. Сервис My Holiday используется во всем приложении только с использованием DI, в том числе внутри функций Twig, в отличие от вызовов методов static, но это можно сделать с помощью КомпиляторПасс в статическом методе для установки дат.

Since you are using static methods to directly access the results, it would require a major application refactor to any service that uses a Holiday static method in favor of using it as a Symfony service.

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

ehymel 29.05.2019 17:46

Другие вопросы по теме