blumfontein / php-business-time
Carbon 日期的商务时间/工作日扩展
Requires
- php: >=7.4
- nesbot/carbon: ^1.2 || ^2.0
Suggests
- guzzlehttp/guzzle: Required for using remote sources
README
PHP中的“商务时间”逻辑(又称“工作小时”、“工作日”等)。例如,这可以用于计算运输日期。
此库为Carbon日期时间库中的Carbon
类提供扩展。
虽然Carbon已经具有diffInWeekendDays()
等方法,但此扩展允许您更精确和灵活地处理商务时间。它可以考虑来自WebCal.fi的公众假期,以及您可以直接指定或通过约束匹配指定的自定义时间。
内容
安装
通过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
这些被分开是因为通常人们不想处理分数工作时间的概念:要么是一个工作日已经过去,要么没有。这些partial
方法允许您在需要时访问浮点数。
工作日长度
要计算部分工作日,我们需要知道一个工作日的总时长。例如,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
类上设置限制以确定工作时间
$businessTime = new BusinessTime\BusinessTime(); $businessTime->setBusinessTimeConstraints( 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->setBusinessTimeConstraints( (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? );
集成远程源的业务时间数据
虽然您可以尝试设置涵盖您国家所有公共假期的约束,但直接从远程源检索它们可能更简单。
BusinessTime 默认支持 WebCal.fi。
WebCal.fi
WebCal.fi 易于使用,因为它的数据是公开提供的,无需任何身份验证。不过,您需要将Guzzle库包含到您的项目中。
$factory = new BusinessTime\Remote\WebCalFiFactory( new GuzzleHttp\Client(), 'https://www.webcal.fi/cal.php?id=83&format=json' // for example ); $dates = $factory->getDates(); // = array of date objects from the specified calendar. $webCalFiConstraint = $factory->makeConstraint(); // = a constraint containing the retrieved dates and their descriptions.
约束将使用您指定的 WebCal.fi 日历中的名称和日期进行设置。
您可以在例如https://www.webcal.fi/en-GB/other_file_formats.php找到满足您需求的 WebCal.fi 日历。
注意,您还可以使用 WebCalFi 约束上的except()
方法为其提供的日期添加自定义异常。
自定义远程源
您可以通过实现上述描述的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
您还可以阅读测试,以获取库更详细的使用示例。