Считать нерабочие дни недели - PullRequest
0 голосов
/ 29 мая 2019

У меня есть рабочий проект 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:
        // /362982/rasschitat-rabochie-dni#363047

        $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;
    }
}

1 Ответ

1 голос
/ 29 мая 2019

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

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

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

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

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 Serializer для генерации, загрузки и сохранения относительных данных в нужном вам формате (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 я добавляю известные относительные и не относительные значения даты в качестве параметров из конфигов к моему праздникусервис, такой как 2001-09-11.Затем я использую службу запросов для вставки неизвестных не относительных дат из базы данных в класс Holiday, таких как закрытие вашего урагана.Сервис My Holiday используется во всем приложении только с использованием DI, в том числе внутри функций Twig, в отличие от вызовов метода static, но это можно сделать с помощью CompilerPass в статическом методе для установкидаты.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...