hughgrigg / php-business-time
Carbon 日期的商务时间/工作日扩展
Requires
- php: ^8.0
- nesbot/carbon: ^1.2 || ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^6.3
- guzzlehttp/psr7: ^1.4
- johnkary/phpunit-speedtrap: ^2.0 || ^3.0
- php-coveralls/php-coveralls: ^2.0
- phpmd/phpmd: @stable
- phpunit/phpunit: ^9.5
- psr/http-message: ^1.0
- squizlabs/php_codesniffer: @stable
Suggests
- guzzlehttp/guzzle: Required for using remote sources
README
PHP中的“商务时间”逻辑(又称“营业时间”、“工作日”等)。例如,这可以用于计算发货日期。
此库为Carbon日期时间库中的Carbon
类提供了一个扩展。
虽然Carbon已经有了像diffInWeekendDays()
这样的方法,但此扩展允许您更精确和灵活地处理商务时间。它可以使用您自定义的时间,这些时间可以直接指定或通过约束匹配来指定。
内容
安装
通过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
您还可以阅读这些测试,以获取该库更详细的用法示例。