计算非假日工作日

时间:2019-05-29 01:51:54

标签: php symfony

我有一个可运行的symfony项目,其中包括一个Holiday类,该类具有几个分组的静态函数,用于计算非假日工作日。代码的核心来自this thread中的几个答案。

我在代码库周围使用静态方法,包括树枝扩展,报告,图表创建等。

最大的缺点是我必须定期使用这些方法并手动输入来年的假期(是的,我知道我可以一次完成数年),但是偶尔会有“假期”我补充说由于天气不好而放假。手动添加一个假期并将代码推送到我的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;
    }
}

1 个答案:

答案 0 :(得分:1)

根据我的建议,在可观察的假期中使用相对格式。我增加了使用可调用对象和日期间隔时间来处理更复杂日期的功能。 我还使用PHP calendar扩展中的easter_date函数将复活节作为假期作为用例。

我还建议按日期对日期进行分段并将其存储到静态属性中,以确定它们是否已被加载,以防止在多次迭代中重新加载它们。

示例: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之类的RDBMS中。然后,可以在标准的可观察到的假期之后(当遇到这些年份时)检索这些年份。

但是,在没有数据库的情况下,您可以使用包含文件,就像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项目中,我将 known 相对和非相对日期值作为配置中的参数添加到我的Holiday服务中,例如2001-09-11。然后,我使用查询服务将数据库中的未知非相对日期注入到Holiday类中,例如飓风关闭。与使用static方法调用相反,我的Holiday服务仅在DI中(包括Twig函数内部)在整个应用程序中使用,但这可以通过在静态方法上使用CompilerPass来进行设置日期。

  

由于您使用静态方法直接访问结果,因此   将需要对使用Holiday的任何服务进行主要的应用程序重构   支持将其用作Symfony服务的静态方法。