hughgrigg/php-business-time

Carbon 日期的商务时间/工作日扩展

3.0.0 2023-03-18 11:52 UTC

This package is auto-updated.

Last update: 2024-09-18 15:02:05 UTC


README

Build Status Coverage Status StyleCI License: MIT

PHP中的“商务时间”逻辑(又称“营业时间”、“工作日”等)。例如,这可以用于计算发货日期。

此库为Carbon日期时间库中的Carbon类提供了一个扩展。

虽然Carbon已经有了像diffInWeekendDays()这样的方法,但此扩展允许您更精确和灵活地处理商务时间。它可以使用您自定义的时间,这些时间可以直接指定或通过约束匹配来指定。

此库的官方音乐视频

在TypeScript中使用BusinessTime

内容

安装

通过Composer安装

composer require hughgrigg/php-business-time

使用

此包中的BusinessTime类扩展了Carbon。这意味着您可以使用所有来自Carbon和本地DateTime的方法,以及这里描述的方法。

工作日

您可能最常处理工作日。

添加或减去工作日

您可以从给定的起始日期添加或减去工作日

$friday = new BusinessTime\BusinessTime('Friday 10am');
$nextBusinessDay = $friday->addBusinessDay();
// = Monday 10am
$threeBusinessDays = $friday->addBusinessDays(3);
// = Wednesday 10am
$monday = new BusinessTime\BusinessTime('Monday 10am');
$previousBusinessDay = $now->subBusinessDay();
// = Friday 10am
$threeBusinessDaysAgo = $now->subBusinessDays(3);
// = Wednesday 10am

工作日差异

除了添加或减去工作日之外,您还可以计算两个给定日期之间的工作日数。

$now = BusinessTime\BusinessTime::now();
$nextWeek = $now->addWeek(); // a full 7-day week.
$diff = $now->diffInBusinessDays($nextWeek);
// = 5

完整工作日与部分工作日

上述示例处理的是完整工作日。您也可以将其描述为整数天数。这意味着任何天数的小数部分都不被视为工作日,也不会被计算。

例如,如果我们询问周五上午10点到周六上午10点之间有多少个工作日,答案是零

$fridayTenAm = new BusinessTime\BusinessTime('Friday 10am');
$saturdayTenAm = $fridayTenAm->addDay(); // Add a full day.
$fridayTenAm->diffInBusinessDays($saturdayTenAm);
// = 0

如果您预期周五的工作时间应该被包括在内,可能会感到惊讶。结果为零的原因是因为在这个时间段内没有通过完整工作日;即使是大部分工作日也不足以被计算。

如果您确实想考虑部分天数,可以使用等效的局部方法来获取浮点值。

$fridayTenAm = new BusinessTime\BusinessTime('Friday 10am');
$fridayTenAm->diffInPartialBusinessDays('Saturday 10am');
// = 0.875

这些被分开处理,因为通常人们不希望处理分数工作日的概念:要么是工作日已经过去,要么没有。局部方法允许您在需要时访问浮点数。

工作日长度

为了计算部分工作日,我们需要知道工作日的总时长。例如,如果工作时间是09:00到17:00,那么这些时间可能是工作日的100%,但如果工作时间是09:00到19:00,那么这些时间可能只占工作日的80%。

默认情况下,BusinessTime将工作日视为8小时长(09:00到17:00)。不过,您可以调整这个设置以适应您的需求。

配置此功能的简单方法是将工作日的长度直接设置

$businessTime = new BusinessTime\BusinessTime();
$businessTime->setLengthOfBusinessDay(BusinessTime\Interval::hours(6));

如果您有复杂的工作时间限制(见下文),让BusinessTime为您计算工作日的长度可能很有帮助。您可以通过将表示您标准工作日的DateTime传递给determineLengthOfBusinessDay()方法来实现。然后,BusinessTime将根据这些限制计算工作日的长度。

$businessTime = new BusinessTime\BusinessTime();
$businessTime->determineLengthOfBusinessDay(new DateTime('Monday'));

工作小时

您还可以按小时进行工作时间计算

$now = new BusinessTime\BusinessTime();
$now->addBusinessHour();
$now->addBusinessHours(3);
$now = new BusinessTime\BusinessTime();
$now->diffInBusinessHours();
$now->diffInPartialBusinessHours();

一天是包含在库中的最大单位,因为人们和组织对更大时间单位的理解不同。没有内置的方法防止了假设,并强制了明确性,例如$now->addBusinessDays(30)

同样,库中没有包含小于小时的单位,因为对于大多数用例来说,“工作分钟”的概念是可疑的。如果您确实需要,可以通过乘以60来计算分钟。请注意,由于默认精度为一个小时,您可能需要将精度调整为例如15分钟以获得准确的计算(见关于精度和性能的说明)。

描述商务时间

在某些情况下,为工作时间和非工作时间提供有意义的描述很有用。例如,您可能想告诉您的客户,因为周末在中间,您将在下周交付他们的订单。

您可以使用BusinessTimePeriod类来实现这一点。您可以像这样创建一个具有开始和结束时间的实例

$start = new BusinessTime\BusinessTime('today');
$end = $start->addBusinessDays(3);
$timePeriod = new BusinessTime\BusinessTimePeriod($start, $end);

然后,您可以使用时间段的businessDays()nonBusinessDays()方法来获取这些信息。例如

$businessDays = $timePeriod->businessDays();
$nonBusinessDays = $timePeriod->nonBusinessDays();

这返回一个包含每个非工作日的BusinessTime对象的数组,可以告诉您它们的名称

$nonBusinessDays[0]->businessName();
// = e.g. "the weekend"

您得到的间隔和描述取决于使用了哪些工作时间限制。

您还可以要求BusinessTimePeriod提供其工作时间和非工作时间子时间段,例如

$start = new BusinessTime\BusinessTime('today');
$end = new BusinessTime\BusinessTime('tomorrow');
$timePeriod = new BusinessTime\BusinessTimePeriod($start, $end);

$businessPeriods = $timePeriod->businessPeriods();
// = array of BusinessTimePeriod instances for each period of business time.
$nonBusinessPeriods = $timePeriod->nonBusinessPeriods();
// = array of BusinessTimePeriod instances for each period of non-business time.

这允许您看到构成整个时间段的业务时间。您可以使用businessName()方法请求每个子时间段与其相关的名称。

工作日的开始和结束

您可以根据业务时间限制以这种方式获取工作日的开始或结束

$businessTime = new BusinessTime\BusinessTime();
$businessTime->startOfBusinessDay();
// = BusinessTime instance for e.g. 09:00
$businessTime->endOfBusinessDay();
// = BusinessTime instance for e.g. 17:00

确定商务时间

默认情况下,此库将周一至周五上午9点至下午5点视为工作时间。尽管如此,您可以根据需要配置此设置。

商务时间约束

您可以通过以下方式在工作时间类上设置约束以确定工作时间

$businessTime = new BusinessTime\BusinessTime();
$businessTime->setConstraints(
    new BusinessTime\Constraint\WeekDays(),
    new BusinessTime\Constraint\BetweenHoursOfDay(9, 17),
);

您可以传递所需的所有约束;所有约束都必须满足,才能将给定时间视为工作时间。

调用setBusinessTimeConstraints()会替换BusinessTime实例上任何现有的约束。

以下是一些默认可用的约束,其中一些可以通过其构造函数进行自定义

new BusinessTime\Constraint\HoursOfDay(10, 13, 17);
new BusinessTime\Constraint\BetweenHoursOfDay(9, 17);
new BusinessTime\Constraint\BetweenTimesOfDay('08:45', '17:30');
new BusinessTime\Constraint\WeekDays();
new BusinessTime\Constraint\Weekends();
new BusinessTime\Constraint\DaysOfWeek('Monday', 'Wednesday', 'Friday');
new BusinessTime\Constraint\BetweenDaysOfWeek('Monday', 'Friday');
new BusinessTime\Constraint\DaysOfMonth(1, 8, 23);
new BusinessTime\Constraint\BetweenDaysOfMonth(1, 20);
new BusinessTime\Constraint\MonthsOfYear('January', 'March', 'July');
new BusinessTime\Constraint\BetweenMonthsOfYear('January', 'November');
new BusinessTime\Constraint\DaysOfYear('January 8th', 'March 16th', 'July 4th');
new BusinessTime\Constraint\BetweenDaysOfYear('January 1st', 'December 5th');
new BusinessTime\Constraint\Dates('2019-01-17', '2019-09-23', '2020-05-11');
new BusinessTime\Constraint\BetweenDates('2018-01-11', '2018-12-31');
new BusinessTime\Constraint\AnyTime(); // Oh dear.

商务时间约束的逆运算

您可以将任何业务时间约束包装在一个Not约束中来反转它。

例如

$decemberOff = new BusinessTime\Constraint\Composite\Not(
    BusinessTime\Constraint\MonthsOfYear('December')
);

此约束现在匹配12月之外的任何时间。您可以将所需的其他约束传递给Not构造函数。

商务时间约束的例外

上述约束有一个except()方法,它接受一个或多个其他约束。这创建了一个复合约束,允许您向业务时间规则添加异常。

例如

$lunchTimeOff = (new BusinessTime\Constraint\BetweenHoursOfDay(9, 17))->except(
    new BusinessTime\Constraint\HoursOfDay(13)
);

该约束现在匹配上午9点至下午5点之间的任何时间,但不是下午1点至下午2点之间的小时。您可以将所需的其他异常约束传递给except()方法。

注意:您可以使用except()方法上的AnyTime约束作为定义约束的替代方法。

(new BusinessTime\Constraint\AnyTime())->except(
    new BusinessTime\Constraint\DaysOfWeek('Friday')
);
// All times except Fridays are considered business time.

如果 except() 无法满足您的需求,您还可以使用 andAlso()orAlternatively() 方法构建不同类型的复合约束。

自定义商务时间约束

您可以通过实现 BusinessTime\Constraint\Constraint 接口来自定义约束。

interface BusinessTimeConstraint
{
    public function isBusinessTime(DateTimeInterface $time): bool;
}

约束必须接受一个 DateTimeInterface 实例,并返回它是否应该被视为工作时间。

如果您想为自定义约束启用组合逻辑,请使用 BusinessTime\Constraint\Composite\Combinations 特性。

提示:通常,使用多个简单约束一起使用比创建一个大而复杂的约束更好。

商务时间约束示例

以下是一个使用业务时间约束的相对复杂的示例。

$businessTime = new BusinessTime\BusinessTime();
$businessTime->setConstraints(
    (new BusinessTime\Constraint\BetweenHoursOfDay(10, 18))->except(
        new BusinessTime\Constraint\BetweenTimesOfDay('13:00', '14:00')
    ), // 9-6 every day, with an hour for lunch.
    (new BusinessTime\Constraint\WeekDays())->except(
        new BusinessTime\Constraint\WeekDays('Thursday')
    ), // Week days, but let's take Thursdays off.
    new BusinessTime\Constraint\BetweenMonthsOfYear('January', 'November'),
    // No-one does any work in December anyway.
    new BusinessTime\Constraint\Composite\Not(
        new BusinessTime\Constraint\DaysOfYear('August 23rd', 'October 20th')
    ) // Why not take off your birthday and wedding anniversary?
);

从远程源集成商务时间数据

虽然您可以尝试设置涵盖您国家所有公共假期的约束,但直接从远程源检索它们可能更容易。

自定义远程源

您可以通过实现上面描述的 Constraint 接口来添加任何其他您喜欢的源。

重复的商务截止日期

除了计算工作时间外,通常还很有用,对截止日期或“截止”时间进行计算。例如,工作日的发货截止时间可能是上午11点。BusinessTime提供了处理这种情况的逻辑。

您可以使用上面描述的相同时间约束来创建截止日期。

$deadline = new BusinessTime\Deadline\RecurringDeadline(
    new BusinessTime\Constraint\Weekdays(),
    new BusinessTime\Constraint\HoursOfDay(11)
);

匹配所有约束的任何时间都视为截止日期的一次出现。这意味着截止日期是定期发生的(它不是时间点)。

要找出截止日期下一次发生的时间,您可以使用 nextOccurrenceFrom() 方法。

$businessTime = new BusinessTime\BusinessTime();
$deadline->nextOccurrenceFrom($businessTime);
// = a new business time instance for the time the deadline next occurs.

在这个例子中,这可能会给您今天上午11点,或者在现在是周五上午11点之后,则是下周一上午11点。

还有一个 previousOccurrenceFrom() 方法,它从给定的时间向前执行等效操作。

您还可以检查在给定时间段内是否已过截止日期。

$deadline->hasPassedToday();
// = true if the deadline has been passed today.
$deadline->hasPassedBetween(
    BusinessTime\BusinessTime::now->subWeek(),
    BusinessTime\BusinessTime::now->addWeek()
);
// = true if the deadline is ever passed in the given time period.

重要:上述截止日期是为了处理重复的截止日期而设计的。它们不适用于确定单一的时间点。要进行比较,您应仅使用Carbon提供的比较方法。

$time = new BusinessTime\BusinessTime();
$deadline = new BusinessTime\BusinessTime('2018-12-08 17:00');
$time->gt($deadline);
// = true if the moment has passed.

商务时间工厂

您可能不想在每个需要使用它的代码位置都设置一个 BusinessTime\BusinessTime 实例。

为了避免这种情况,您可以将一次设置所需约束的 BusinessTime\Factory,然后在任何地方使用它。

例如

$factory = new BusinessTime\BusinessTimeFactory();
$factory->setConstraints(
    new BusinessTime\Constraint\DaysOfWeek('Saturday', 'Sunday'),
    new BusinessTime\Constraint\Dates('2018-12-25'),
);

一旦设置好工厂,您就可以以您通常共享依赖项的方式共享它。例如,您可能将其添加到Laravel或Symfony等框架的容器中。

当您获得工厂的实例时,您可以从它那里获取一个现成的 BusinessTime\BusinessTime 实例。

$date = $factory->make('2018-03-21');
$now = $factory->now();

BusinessTimeFactory 实例可以序列化,这使得将其存储在缓存或文件系统中变得容易。

精度

默认情况下,BusinessTime使用小时精度。这意味着它计算的工作时间大约精确到一小时。

如果您需要比这更高的精度,您可以将其设置为所需的精度。

$businessTime = new BusinessTime\BusinessTime();
$businessTime->setPrecision(BusinessTime\Interval::minutes(30)); // Half-hour precision.
$businessTime->setPrecision(BusinessTime\Interval::minutes(15)); // Quarter-hour precision.

您也可以以相同的方式在业务时间工厂上设置精度。

请注意,精度越高,性能越低。这是因为BusinessTime必须检查您指定的每个大小的时间间隔。例如,在小时精度下,处理一周需要 7 * 24 = 168 次迭代。在分钟精度下,这变为 7 * 24 * 60 = 10080 次迭代,这比慢60倍。

始终尝试设置覆盖您需求的最大精度间隔。

测试

您可以使用Carbon中描述的测试功能

http://carbon.nesbot.com/docs/#api-testing

例如,您可以模拟当前时间如下

Carbon::setTestNow($knownDate);

然后继续进行测试。

要运行 BusinessTime 包本身的测试,您可以在该目录下运行测试。

make test

您还可以阅读这些测试,以获取该库更详细的用法示例。