From 74654133659a5cdc59fdda51f7ea545056d505e3 Mon Sep 17 00:00:00 2001 From: someniatko Date: Fri, 12 Aug 2022 11:45:25 +0300 Subject: [PATCH 1/3] implement `contains`, `intersectsWith`, `getIntersectionWith`, `::of` for `Instant` --- src/Interval.php | 49 ++++++++++++++ tests/IntervalTest.php | 143 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/src/Interval.php b/src/Interval.php index fd5f789..c614234 100644 --- a/src/Interval.php +++ b/src/Interval.php @@ -40,6 +40,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. */ @@ -84,6 +95,44 @@ 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 new Interval($latestStart, $earliestEnd); + } + /** * Serializes as a string using {@see Interval::__toString()}. */ diff --git a/tests/IntervalTest.php b/tests/IntervalTest.php index e0149bd..38fefb0 100644 --- a/tests/IntervalTest.php +++ b/tests/IntervalTest.php @@ -37,6 +37,17 @@ public function testGetStartEnd(): void $this->assertInstantIs(2000000009, 123456789, $interval->getEnd()); } + public function testGetStartEndUsingOf(): 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()); + } + /** * @depends testGetStartEnd */ @@ -95,6 +106,138 @@ 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 = new Interval(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 = new Interval(Instant::of($start1), Instant::of($end1)); + $interval2 = new Interval(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 = new Interval(Instant::of($start1), Instant::of($end1)); + $interval2 = new Interval(Instant::of($start2), Instant::of($end2)); + $expected = new Interval(Instant::of($expectedStart), Instant::of($expectedEnd)); + + $this->assertEquals($expected, $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 = new Interval(Instant::of(100000), Instant::of(200000)); + $interval2 = new Interval(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); + } + public function testJsonSerialize(): void { $interval = new Interval( From e88378b512135241894c0dd9e32550865208e160 Mon Sep 17 00:00:00 2001 From: someniatko Date: Thu, 18 May 2023 11:26:49 +0300 Subject: [PATCH 2/3] Implement `Interval::isEqualTo()` --- src/Interval.php | 6 ++++++ tests/IntervalTest.php | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Interval.php b/src/Interval.php index c614234..0db7a55 100644 --- a/src/Interval.php +++ b/src/Interval.php @@ -133,6 +133,12 @@ public function getIntersectionWith(Interval $that): Interval return new Interval($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()}. */ diff --git a/tests/IntervalTest.php b/tests/IntervalTest.php index 38fefb0..3bb23bd 100644 --- a/tests/IntervalTest.php +++ b/tests/IntervalTest.php @@ -193,7 +193,7 @@ public function testGetIntersectionWith( $interval2 = new Interval(Instant::of($start2), Instant::of($end2)); $expected = new Interval(Instant::of($expectedStart), Instant::of($expectedEnd)); - $this->assertEquals($expected, $interval1->getIntersectionWith($interval2)); + $this->assertTrue($expected->isEqualTo($interval1->getIntersectionWith($interval2))); } public function providerGetIntersectionWith(): array @@ -238,6 +238,39 @@ public function testGetIntersectionWithInvalidParams(): void $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( From a975807657e92f560d4d1384659702890e561f83 Mon Sep 17 00:00:00 2001 From: someniatko Date: Thu, 18 May 2023 11:28:41 +0300 Subject: [PATCH 3/3] Deprecate `Interval::__construct()` in favor of `Interval::of()` --- src/Interval.php | 8 +++++--- tests/IntervalTest.php | 34 +++++++++++++++++----------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/Interval.php b/src/Interval.php index 0db7a55..8dedea2 100644 --- a/src/Interval.php +++ b/src/Interval.php @@ -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. * @@ -74,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); } /** @@ -84,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); } /** @@ -130,7 +132,7 @@ public function getIntersectionWith(Interval $that): Interval $latestStart = $this->start->isAfter($that->start) ? $this->start : $that->start; $earliestEnd = $this->end->isBefore($that->end) ? $this->end : $that->end; - return new Interval($latestStart, $earliestEnd); + return Interval::of($latestStart, $earliestEnd); } public function isEqualTo(Interval $that): bool diff --git a/tests/IntervalTest.php b/tests/IntervalTest.php index 3bb23bd..1049097 100644 --- a/tests/IntervalTest.php +++ b/tests/IntervalTest.php @@ -23,7 +23,7 @@ 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 @@ -31,18 +31,18 @@ public function testGetStartEnd(): void $start = Instant::of(2000000000, 987654321); $end = Instant::of(2000000009, 123456789); - $interval = new Interval($start, $end); + $interval = Interval::of($start, $end); $this->assertInstantIs(2000000000, 987654321, $interval->getStart()); $this->assertInstantIs(2000000009, 123456789, $interval->getEnd()); } - public function testGetStartEndUsingOf(): void + public function testGetStartEndUsingDeprecatedPublicConstructor(): void { $start = Instant::of(2000000000, 987654321); $end = Instant::of(2000000009, 123456789); - $interval = Interval::of($start, $end); + $interval = new Interval($start, $end); $this->assertInstantIs(2000000000, 987654321, $interval->getStart()); $this->assertInstantIs(2000000009, 123456789, $interval->getEnd()); @@ -53,7 +53,7 @@ public function testGetStartEndUsingOf(): void */ public function testWithStart(): void { - $interval = new Interval( + $interval = Interval::of( Instant::of(2000000000), Instant::of(2000000001) ); @@ -76,7 +76,7 @@ public function testWithStart(): void */ public function testWithEnd(): void { - $interval = new Interval( + $interval = Interval::of( Instant::of(2000000000), Instant::of(2000000001) ); @@ -96,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) ); @@ -109,7 +109,7 @@ public function testGetDuration(): void /** @dataProvider providerContains */ public function testContains(int $start, int $end, int $now, bool $expected, string $errorMessage): void { - $interval = new Interval(Instant::of($start), Instant::of($end)); + $interval = Interval::of(Instant::of($start), Instant::of($end)); $this->assertSame($expected, $interval->contains(Instant::of($now)), $errorMessage); } @@ -144,8 +144,8 @@ public function providerContains(): array /** @dataProvider providerIntersectsWith */ public function testIntersectsWith(int $start1, int $end1, int $start2, int $end2, bool $expected): void { - $interval1 = new Interval(Instant::of($start1), Instant::of($end1)); - $interval2 = new Interval(Instant::of($start2), Instant::of($end2)); + $interval1 = Interval::of(Instant::of($start1), Instant::of($end1)); + $interval2 = Interval::of(Instant::of($start2), Instant::of($end2)); $this->assertSame($expected, $interval1->intersectsWith($interval2)); } @@ -189,9 +189,9 @@ public function testGetIntersectionWith( int $expectedStart, int $expectedEnd ): void { - $interval1 = new Interval(Instant::of($start1), Instant::of($end1)); - $interval2 = new Interval(Instant::of($start2), Instant::of($end2)); - $expected = new Interval(Instant::of($expectedStart), Instant::of($expectedEnd)); + $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))); } @@ -229,8 +229,8 @@ public function providerGetIntersectionWith(): array public function testGetIntersectionWithInvalidParams(): void { - $interval1 = new Interval(Instant::of(100000), Instant::of(200000)); - $interval2 = new Interval(Instant::of(300000), Instant::of(400000)); + $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.'); @@ -273,7 +273,7 @@ public function providerIsEqualTo(): array public function testJsonSerialize(): void { - $interval = new Interval( + $interval = Interval::of( Instant::of(1000000000), Instant::of(2000000000) ); @@ -283,7 +283,7 @@ public function testJsonSerialize(): void public function testToString(): void { - $interval = new Interval( + $interval = Interval::of( Instant::of(1000000000), Instant::of(2000000000) );