Skip to content

Commit

Permalink
Merge pull request #64 from someniatko/interval-enhancements
Browse files Browse the repository at this point in the history
Implement contains, intersection and `of` methods for Interval
  • Loading branch information
BenMorel authored May 19, 2023
2 parents d84d2da + a975807 commit 8739cbb
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 8 deletions.
61 changes: 59 additions & 2 deletions src/Interval.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ final class Interval implements JsonSerializable
private Instant $end;

/**
* @deprecated Use {@see Interval::of()} instead.
*
* @param Instant $startInclusive The start instant, inclusive.
* @param Instant $endExclusive The end instant, exclusive.
*
Expand All @@ -40,6 +42,17 @@ public function __construct(Instant $startInclusive, Instant $endExclusive)
$this->end = $endExclusive;
}

/**
* @param Instant $startInclusive The start instant, inclusive.
* @param Instant $endExclusive The end instant, exclusive.
*
* @throws DateTimeException If the end instant is before the start instant.
*/
public static function of(Instant $startInclusive, Instant $endExclusive): Interval
{
return new Interval($startInclusive, $endExclusive);
}

/**
* Returns the start instant, inclusive, of this Interval.
*/
Expand All @@ -63,7 +76,7 @@ public function getEnd(): Instant
*/
public function withStart(Instant $start): Interval
{
return new Interval($start, $this->end);
return Interval::of($start, $this->end);
}

/**
Expand All @@ -73,7 +86,7 @@ public function withStart(Instant $start): Interval
*/
public function withEnd(Instant $end): Interval
{
return new Interval($this->start, $end);
return Interval::of($this->start, $end);
}

/**
Expand All @@ -84,6 +97,50 @@ public function getDuration(): Duration
return Duration::between($this->start, $this->end);
}

/**
* Returns whether this Interval contains the given Instant.
*/
public function contains(Instant $instant): bool
{
return $instant->isAfterOrEqualTo($this->start)
&& $instant->isBefore($this->end);
}

/**
* Returns whether this Interval intersects with the given one.
*/
public function intersectsWith(Interval $that): bool
{
[$prev, $next] = $this->start->isBefore($that->start)
? [$this, $that]
: [$that, $this];

return $next->start->isBefore($prev->end);
}

/**
* Returns an Interval which is an intersection of this one with the given one.
*
* @throws DateTimeException If the Intervals do not intersect.
*/
public function getIntersectionWith(Interval $that): Interval
{
if (! $this->intersectsWith($that)) {
throw new DateTimeException('Intervals "' . $this . '" and "' . $that . '" do not intersect.');
}

$latestStart = $this->start->isAfter($that->start) ? $this->start : $that->start;
$earliestEnd = $this->end->isBefore($that->end) ? $this->end : $that->end;

return Interval::of($latestStart, $earliestEnd);
}

public function isEqualTo(Interval $that): bool
{
return $this->start->isEqualTo($that->start)
&& $this->end->isEqualTo($that->end);
}

/**
* Serializes as a string using {@see Interval::__toString()}.
*/
Expand Down
188 changes: 182 additions & 6 deletions tests/IntervalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,25 @@ public function testEndInstantIsNotBeforeStartInstant(): void
$this->expectException(DateTimeException::class);
$this->expectExceptionMessage('The end instant must not be before the start instant.');

new Interval($end, $start);
Interval::of($end, $start);
}

public function testGetStartEnd(): void
{
$start = Instant::of(2000000000, 987654321);
$end = Instant::of(2000000009, 123456789);

$interval = Interval::of($start, $end);

$this->assertInstantIs(2000000000, 987654321, $interval->getStart());
$this->assertInstantIs(2000000009, 123456789, $interval->getEnd());
}

public function testGetStartEndUsingDeprecatedPublicConstructor(): void
{
$start = Instant::of(2000000000, 987654321);
$end = Instant::of(2000000009, 123456789);

$interval = new Interval($start, $end);

$this->assertInstantIs(2000000000, 987654321, $interval->getStart());
Expand All @@ -42,7 +53,7 @@ public function testGetStartEnd(): void
*/
public function testWithStart(): void
{
$interval = new Interval(
$interval = Interval::of(
Instant::of(2000000000),
Instant::of(2000000001)
);
Expand All @@ -65,7 +76,7 @@ public function testWithStart(): void
*/
public function testWithEnd(): void
{
$interval = new Interval(
$interval = Interval::of(
Instant::of(2000000000),
Instant::of(2000000001)
);
Expand All @@ -85,7 +96,7 @@ public function testWithEnd(): void

public function testGetDuration(): void
{
$interval = new Interval(
$interval = Interval::of(
Instant::of(1999999999, 555555),
Instant::of(2000000001, 111)
);
Expand All @@ -95,9 +106,174 @@ public function testGetDuration(): void
$this->assertDurationIs(1, 999444556, $duration);
}

/** @dataProvider providerContains */
public function testContains(int $start, int $end, int $now, bool $expected, string $errorMessage): void
{
$interval = Interval::of(Instant::of($start), Instant::of($end));

$this->assertSame($expected, $interval->contains(Instant::of($now)), $errorMessage);
}

public function providerContains(): array
{
return [
'at the start' => [
1000000000,
2000000000,
1000000000,
true,
'an Interval must contain its start',
],
'at the end' => [
1000000000,
2000000000,
2000000000,
false,
'an Interval must not contain its end',
],
'in the middle' => [
1000000001,
1000000003,
1000000002,
true,
'an Interval must contain its intermediate values',
],
];
}

/** @dataProvider providerIntersectsWith */
public function testIntersectsWith(int $start1, int $end1, int $start2, int $end2, bool $expected): void
{
$interval1 = Interval::of(Instant::of($start1), Instant::of($end1));
$interval2 = Interval::of(Instant::of($start2), Instant::of($end2));
$this->assertSame($expected, $interval1->intersectsWith($interval2));
}

public function providerIntersectsWith(): array
{
return [
'second is after first' => [
100000, 200000,
400000, 500000,
false,
],
'second is before first' => [
400000, 500000,
100000, 200000,
false,
],
'end of the first is start of the second' => [
100000, 200000,
200000, 300000,
false,
],
'start of the first is end of the second' => [
200000, 300000,
100000, 200000,
false,
],
'intersection' => [
100000, 200000,
150000, 250000,
true,
],
];
}

/** @dataProvider providerGetIntersectionWith */
public function testGetIntersectionWith(
int $start1,
int $end1,
int $start2,
int $end2,
int $expectedStart,
int $expectedEnd
): void {
$interval1 = Interval::of(Instant::of($start1), Instant::of($end1));
$interval2 = Interval::of(Instant::of($start2), Instant::of($end2));
$expected = Interval::of(Instant::of($expectedStart), Instant::of($expectedEnd));

$this->assertTrue($expected->isEqualTo($interval1->getIntersectionWith($interval2)));
}

public function providerGetIntersectionWith(): array
{
return [
'first before second' => [
100000, 200000,
150000, 250000,
150000, 200000,
],
'first after second' => [
150000, 250000,
100000, 200000,
150000, 200000,
],
'first inside second' => [
200000, 300000,
100000, 400000,
200000, 300000,
],
'second inside first' => [
100000, 400000,
200000, 300000,
200000, 300000,
],
'first = second' => [
5000, 6000,
5000, 6000,
5000, 6000,
],
];
}

public function testGetIntersectionWithInvalidParams(): void
{
$interval1 = Interval::of(Instant::of(100000), Instant::of(200000));
$interval2 = Interval::of(Instant::of(300000), Instant::of(400000));

$this->expectException(DateTimeException::class);
$this->expectExceptionMessage('Intervals "1970-01-02T03:46:40Z/1970-01-03T07:33:20Z" and "1970-01-04T11:20Z/1970-01-05T15:06:40Z" do not intersect.');

$interval1->getIntersectionWith($interval2);
}

/** @dataProvider providerIsEqualTo */
public function testIsEqualTo(Interval $a, Interval $b, bool $expectedResult): void
{
$this->assertSame($expectedResult, $a->isEqualTo($b));
$this->assertSame($expectedResult, $b->isEqualTo($a));
}

public function providerIsEqualTo(): array
{
return [
'start is not equal' => [
Interval::of(Instant::of(100000), Instant::of(200000)),
Interval::of(Instant::of(150000), Instant::of(200000)),
false,
],
'end is not equal' => [
Interval::of(Instant::of(100000), Instant::of(200000)),
Interval::of(Instant::of(100000), Instant::of(250000)),
false,
],
'both start and end are not equal' => [
Interval::of(Instant::of(100000), Instant::of(200000)),
Interval::of(Instant::of(150000), Instant::of(250000)),
false,
],
'intervals are equal' => [
Interval::of(Instant::of(100000), Instant::of(200000)),
Interval::of(Instant::of(100000), Instant::of(200000)),
true,
],
];
}

public function testJsonSerialize(): void
{
$interval = new Interval(
$interval = Interval::of(
Instant::of(1000000000),
Instant::of(2000000000)
);
Expand All @@ -107,7 +283,7 @@ public function testJsonSerialize(): void

public function testToString(): void
{
$interval = new Interval(
$interval = Interval::of(
Instant::of(1000000000),
Instant::of(2000000000)
);
Expand Down

0 comments on commit 8739cbb

Please sign in to comment.