Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into scrutinizer
Browse files Browse the repository at this point in the history
  • Loading branch information
hughgrigg committed Apr 9, 2018
2 parents 5f2cfd4 + 6cff88b commit 73c8b0f
Show file tree
Hide file tree
Showing 33 changed files with 1,283 additions and 124 deletions.
8 changes: 8 additions & 0 deletions .scrutinizer.yml
@@ -0,0 +1,8 @@
build:
tests:
override:
-
command: 'vendor/bin/phpunit --coverage-clover=build/logs/clover.xml'
coverage:
file: 'build/logs/clover.xml'
format: 'clover'
98 changes: 53 additions & 45 deletions README.md
Expand Up @@ -3,6 +3,9 @@
[![Build Status](https://travis-ci.org/hughgrigg/php-business-time.svg?branch=master)](https://travis-ci.org/hughgrigg/php-business-time)
[![Coverage Status](https://coveralls.io/repos/github/hughgrigg/php-business-time/badge.svg)](https://coveralls.io/github/hughgrigg/php-business-time)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/ef5b774bce624ab2b1f3632e9307a909)](https://app.codacy.com/app/hugh_2/php-business-time?utm_source=github.com&utm_medium=referral&utm_content=hughgrigg/php-business-time&utm_campaign=badger)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/hughgrigg/php-business-time/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/hughgrigg/php-business-time/?branch=master)
[![StyleCI](https://styleci.io/repos/126614310/shield?branch=master)](https://styleci.io/repos/126614310)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

"Business time" logic in PHP (aka "business hours", "working days" etc). This
can be useful for calculating shipping dates, for example.
Expand Down Expand Up @@ -161,6 +164,56 @@ precision is one hour, you may well need to adjust the precision to e.g 15
minutes to get accurate calculations (see the note on precision and
performance).

## Describing business times

In some situations it's useful to have meaningful descriptions for business and
non-business times. For example, you might want to tell your customer that you
won't deliver their order until next week because the weekend is in between.

You can use the `BusinessTimePeriod` class for this. You can make an instance
with start and end times like this:

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

You can then use the `businessDaysTo()` and `nonBusinessDaysTo()` methods on the
time period to get that information. For example:

```php
$businessTime = new BusinessTime\BusinessTime();
$nonBusinessTimes = $businessTime->nonBusinessDays();
```

This returns an array of `BusinessTime` objects for each non-business day, which
can tell you their description:

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

What intervals and descriptions you get depends on which business time
constraints have been used.

You can also ask a `BusinessTimePeriod` for its business and non-business sub-
periods, for example:

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

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

This lets you see the precise business timings that make up the whole time
period. You can ask each sub-period for its business-relevant name with the
`businessName()` method.

## Determining business time

By default, this library considers Monday to Friday, 9am to 5pm to be business
Expand Down Expand Up @@ -295,51 +348,6 @@ $businessTime->setBusinessTimeConstraints(
);
```

#### Describing business time constraints (WIP)

In some situations it's useful to have meaningful descriptions for business and
non-business times. For example, you might want to tell your customer that you
won't deliver their order until next week because the weekend is in between.

You can use the `businessTimesTo()` and `nonBusinessTimesTo()` methods
to get this. For example:

```php
$businessTime = new BusinessTime\BusinessTime();
$nonBusinessTimes = $businessTime->nonBusinessTimesTo(Carbon::now()->addWeek());
```

This returns an array of `BusinessTimePeriod` objects, which can tell you
their description and timings:

```php
$nonBusinessTimes[0]->businessTimeDescription();
// = "the weekend"
$nonBusinessTimes[0]->startTime();
// = BusinessTime object for Friday 17:00
$nonBusinessTimes[0]->endTime();
// = BusinessTime object for Monday 09:00
```

What intervals and descriptions you get depends on which business time
constraints have been used.

##### Custom business time constraint descriptions

The constraints above come with reasonable default names, but you can also
specify your own with the `setBusinessTimeDescription()` method:

```php
$weekDays = new BusinessTime\Constraint\Weekdays();
$weekDays->setBusinessTimeDescription('working days', 'weekend party time');
```

The second argument is the description for times that *don't* match the
constraint (and is therefore probably more important).

*Note*: if you're using a translation system, then pass in your translation keys
as the descriptions, then fetch the translations for them at run time.

## Incorporating business time data from a remote source (WIP)

Whilst you could try to set up constraints covering all the public holidays in
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -14,7 +14,7 @@
"johnkary/phpunit-speedtrap": "^3.0",
"php-coveralls/php-coveralls": "^2.0"
},
"license": "MIT License",
"license": "MIT",
"authors": [
{
"name": "Hugh Grigg",
Expand Down
115 changes: 98 additions & 17 deletions src/BusinessTime.php
Expand Up @@ -2,9 +2,12 @@

namespace BusinessTime;

use BusinessTime\Constraint\AnyTime;
use BusinessTime\Constraint\BetweenHoursOfDay;
use BusinessTime\Constraint\BusinessTimeConstraint;
use BusinessTime\Constraint\Composite\All;
use BusinessTime\Constraint\Narration\BusinessTimeNarrator;
use BusinessTime\Constraint\Narration\DefaultNarrator;
use BusinessTime\Constraint\WeekDays;
use Carbon\Carbon;
use DateInterval;
Expand Down Expand Up @@ -36,6 +39,16 @@ public function isBusinessTime(): bool
return $this->businessTimeConstraints()->isBusinessTime($this);
}

/**
* Is it a business day?
*
* @return bool
*/
public function isBusinessDay(): bool
{
return $this->startOfBusinessDay()->isSameDay($this);
}

/**
* Get the date time after adding one whole business day.
*
Expand Down Expand Up @@ -71,20 +84,22 @@ public function addBusinessDays(float $businessDaysToAdd): self
}

// Jump ahead in whole days first, because the business days to add
// will be at least this much. This also solves the "intuitive
// problem" that Monday 09:00 + 1 business day could technically be
// Monday 17:00, but intuitively should be Tuesday 09:00.
// will be at least this much. This solves the "intuitive problem" that
// Monday 09:00 + 1 business day could technically be Monday 17:00, but
// intuitively should be Tuesday 09:00.
$daysToJump = (int) $businessDaysToAdd;
/** @var BusinessTime $next */
$next = $this->copy()->addDays($daysToJump);

// We need to check how much business time we actually covered by
// skipping ahead in days.
$businessDaysToAdd -= $this->diffInPartialBusinessDays($next);

$decrement = $this->precision()->inDays()
/ $this->lengthOfBusinessDay()->inDays();
/ $this->lengthOfBusinessDay()->inDays();

while ($businessDaysToAdd > 0) {
/* @scrutinizer ignore-call */
if ($next->isBusinessTime()) {
$businessDaysToAdd -= $decrement;
}
Expand Down Expand Up @@ -137,7 +152,7 @@ public function subBusinessDays(float $businessDaysToSub): self
$businessDaysToSub -= $this->diffInPartialBusinessDays($prev);

$decrement = $this->precision()->inDays()
/ $this->lengthOfBusinessDay()->inDays();
/ $this->lengthOfBusinessDay()->inDays();

while ($businessDaysToSub > 0) {
$prev = $prev->sub($this->precision());
Expand Down Expand Up @@ -180,9 +195,11 @@ public function addBusinessHours(float $businessHoursToAdd): self
return $this->subBusinessHours($businessHoursToAdd * -1);
}

/** @var BusinessTime $next */
$next = $this->copy();
$decrement = $this->precision()->inHours();
while ($businessHoursToAdd > 0) {
/* @scrutinizer ignore-call */
if ($next->isBusinessTime()) {
$businessHoursToAdd -= $decrement;
}
Expand Down Expand Up @@ -289,7 +306,7 @@ public function diffInPartialBusinessDays(
bool $absolute = true
): float {
return $this->diffInBusinessTime($time, $absolute)
/ $this->lengthOfBusinessDay()->asMultipleOf($this->precision());
/ $this->lengthOfBusinessDay()->asMultipleOf($this->precision());
}

/**
Expand All @@ -308,7 +325,7 @@ public function diffInPartialBusinessHours(
bool $absolute = true
): float {
return $this->diffInBusinessTime($time, $absolute)
* $this->precision()->inHours();
* $this->precision()->inHours();
}

/**
Expand All @@ -333,6 +350,14 @@ public function diffBusiness(
);
}

/**
* @return string
*/
public function businessName(): string
{
return $this->canonicalNarrator()->narrate($this);
}

/**
* Get the first business time after the start of this day.
*
Expand Down Expand Up @@ -382,10 +407,10 @@ public function endOfBusinessDay(): self
public function floor(?DateInterval $precision = null): self
{
$seconds = Interval::instance($precision ?: $this->precision())
->inSeconds();
->inSeconds();

return $this->copy()->setTimestamp(
floor($this->timestamp / $seconds) * $seconds
(int) (floor($this->timestamp / $seconds) * $seconds)
);
}

Expand All @@ -401,10 +426,10 @@ public function floor(?DateInterval $precision = null): self
public function round(?DateInterval $precision = null): self
{
$seconds = Interval::instance($precision ?: $this->precision())
->inSeconds();
->inSeconds();

return $this->copy()->setTimestamp(
round($this->timestamp / $seconds) * $seconds
(int) (round($this->timestamp / $seconds) * $seconds)
);
}

Expand All @@ -423,10 +448,10 @@ public function round(?DateInterval $precision = null): self
public function ceil(?DateInterval $precision = null): self
{
$seconds = Interval::instance($precision ?: $this->precision())
->inSeconds();
->inSeconds();

return $this->copy()->setTimestamp(
ceil($this->timestamp / $seconds) * $seconds
(int) (ceil($this->timestamp / $seconds) * $seconds)
);
}

Expand Down Expand Up @@ -497,8 +522,8 @@ public function determineLengthOfBusinessDay(

return $this->setLengthOfBusinessDay(
$this->copy()
->setTimestamp($typicalDay->startOfDay()->getTimestamp())
->diffBusiness($typicalDay->endOfDay())
->setTimestamp($typicalDay->startOfDay()->getTimestamp())
->diffBusiness($typicalDay->endOfDay())
);
}

Expand All @@ -518,9 +543,9 @@ public function setBusinessTimeConstraints(
/**
* Get the business time constraints.
*
* @return BusinessTimeConstraint
* @return All|BusinessTimeConstraint[]
*/
public function businessTimeConstraints(): BusinessTimeConstraint
public function businessTimeConstraints(): All
{
if ($this->businessTimeConstraints === null) {
// Default to week days 09:00 - 17:00.
Expand Down Expand Up @@ -575,6 +600,31 @@ public function setPrecision(DateInterval $precision): self
return $this;
}

/**
* Guarantee copy is instance of BusinessTime to deter analyser complaints.
*
* @return BusinessTime
*/
public function copy(): self
{
$copy = clone $this;
\assert($copy instanceof self);

return $copy;
}

/**
* @param DateTimeInterface $dti
*
* @return BusinessTime
*/
public static function fromDti(DateTimeInterface $dti): self
{
return (new static())
->setTimezone($dti->getTimezone())
->setTimestamp($dti->getTimestamp());
}

/**
* Difference in business time measured in units of the current precision.
*
Expand All @@ -594,6 +644,7 @@ private function diffInBusinessTime(
// We're taking a basic approach with some variables and a loop here as
// it turns out to be ~25% faster than using Carbon::diffFiltered().

/** @var BusinessTime $start */
$start = $this;
$end = $time;
$sign = 1;
Expand All @@ -608,8 +659,11 @@ private function diffInBusinessTime(
// Count the business time diff by iterating in steps the length of the
// precision and checking if each step counts as business time.
$diff = 0;
/** @var BusinessTime $next */
/** @scrutinizer ignore-call */
$next = $start->copy();
while ($next < $end) {
/* @scrutinizer ignore-call */
if ($next->isBusinessTime()) {
$diff++;
}
Expand All @@ -618,4 +672,31 @@ private function diffInBusinessTime(

return $diff * $sign;
}

/**
* Get a narrator for the first business time constraint that determines
* whether this time is business time or not.
*
* @return BusinessTimeNarrator
*/
private function canonicalNarrator(): BusinessTimeNarrator
{
/* @var BusinessTimeConstraint $constraint */
if (!$this->isBusinessTime()) {
foreach ($this->businessTimeConstraints() as $constraint) {
if (!$constraint->isBusinessTime($this)) {
return new DefaultNarrator($constraint);
}
}
}
if ($this->isBusinessTime()) {
foreach ($this->businessTimeConstraints() as $constraint) {
if ($constraint->isBusinessTime($this)) {
return new DefaultNarrator($constraint);
}
}
}

return new AnyTime();
}
}

0 comments on commit 73c8b0f

Please sign in to comment.