Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement contains, intersection and of methods for Interval #64

Merged
merged 3 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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