【问题标题】:Count non-holiday weekdays计算非节假日工作日
【发布时间】:2023-03-08 16:35:01
【问题描述】:

我有一个工作的 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;
    }
}

【问题讨论】:

  • 您将希望将relative dates 用于您的假期,例如fourth Thursday of Novemberfourth Thurdsay of November + 1 day,并使用要影响的年份修改DateTimeImmutale 对象。喜欢:3v4l.org/pra1L 我创建了一个类似的服务来做同样的事情,但我受到 NDA 的约束,否则我会提供更多帮助。
  • 感谢@fyrye,但这对于随机的日子(例如因恶劣天气而关闭)无济于事,因此失去了数据库的灵活性。
  • 这是意料之中的,因为从技术上讲,组织关闭不是一个可观察到的假期,也不是静态定义的。但是很容易解决并且可以在没有数据库的情况下定义,例如读取包含文件中$_POST 值数组的CSVvar_export。例如:$holidays = include '/path/to/file.php'; 和 file.php 包含 return array('....');

标签: php symfony


【解决方案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 values。

$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。然后,我使用查询服务将数据库中的 unknown 非相对日期注入到 Holiday 类中,例如飓风关闭。我的假期服务在整个应用程序中仅使用 DI 使用,包括在 Twig 函数内部,而不是使用 static 方法调用,但这可以通过在静态方法上使用 CompilerPass 来设置日期。

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

【讨论】:

  • 感谢您的深入回答。我接受了您对CLOSURES_FILE 的建议,这非常符合我的需求,只需对我的代码库进行最少的修改。
猜你喜欢
  • 2018-09-09
  • 1970-01-01
  • 1970-01-01
  • 2017-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-13
  • 2014-07-15
相关资源
最近更新 更多