From 4dcbc43009ccd3096745d3275e6363938e67d6c8 Mon Sep 17 00:00:00 2001 From: Joby Elliott Date: Mon, 29 Jul 2024 11:06:00 -0600 Subject: [PATCH] added boolean AND, updated tests --- src/Ranges/AbstractRange.php | 22 +- tests/Ranges/IntegerRangeTest.php | 343 ++++++++++++++++++++++++++++-- 2 files changed, 342 insertions(+), 23 deletions(-) diff --git a/src/Ranges/AbstractRange.php b/src/Ranges/AbstractRange.php index d6290f2..c38a6a8 100644 --- a/src/Ranges/AbstractRange.php +++ b/src/Ranges/AbstractRange.php @@ -68,13 +68,31 @@ abstract class AbstractRange * @param T|null $start * @param T|null $end */ - public function __construct($start, $end) + final public function __construct($start, $end) { $this->setStart($start); $this->setEnd($end); } /** + * Perform a boolean AND operation on this range and another, returning the + * range that they both cover, returns null if they do not overlap. + * @param static $other + */ + public function booleanAnd(AbstractRange $other): static|null + { + if ($this->contains($other)) return new static($other->start(), $other->end()); + elseif ($other->contains($this)) return new static($this->start(), $this->end()); + elseif ($this->intersects($other)) { + return new static( + $this->extendsBefore($other) ? $other->start() : $this->start(), + $this->extendsAfter($other) ? $other->end() : $this->end() + ); + } else return null; + } + + /** + * Check if this range has the same start and end as another range. * @param static $other */ public function equals(AbstractRange $other): bool @@ -83,6 +101,7 @@ abstract class AbstractRange } /** + * Check if any part of this range overlaps with another range. * @param static $other */ public function intersects(AbstractRange $other): bool @@ -93,6 +112,7 @@ abstract class AbstractRange } /** + * Check if this range completely contains another range. * @param static $other */ public function contains(AbstractRange $other): bool diff --git a/tests/Ranges/IntegerRangeTest.php b/tests/Ranges/IntegerRangeTest.php index 2041c34..8746d3c 100644 --- a/tests/Ranges/IntegerRangeTest.php +++ b/tests/Ranges/IntegerRangeTest.php @@ -25,7 +25,6 @@ namespace Joby\Toolbox\Sorting; -use Exception; use Joby\Toolbox\Ranges\IntegerRange; use PHPUnit\Framework\TestCase; @@ -341,7 +340,7 @@ class IntegerRangeTest extends TestCase 'contains', ) ); - // fully unbounded + // fully unbounded contains anything $this->assertEquals( [ 'same' => true, @@ -356,13 +355,109 @@ class IntegerRangeTest extends TestCase ); } - // TODO: testAbutsEndOf - - // TODO: see about making failures go to the right line/test + public function testAbutsEndOf() + { + // fully bounded only abuts the end of things that are adjacent left + $this->assertEquals( + [ + 'same' => false, + 'unbounded' => false, + 'intersecting open end left' => false, + 'intersecting open end right' => false, + 'intersecting open end center' => false, + 'intersecting open start left' => false, + 'intersecting open start right' => false, + 'intersecting open start center' => false, + 'disjoint open start' => false, + 'disjoint open end' => false, + 'disjoint left' => false, + 'disjoint right' => false, + 'adjacent open start' => true, + 'adjacent open end' => false, + 'adjacent left' => true, + 'adjacent right' => false, + 'contained' => false, + 'contained same start' => false, + 'contained same end' => false, + 'containing' => false, + 'containing unbounded start' => false, + 'containing unbounded end' => false, + 'containing same start' => false, + 'containing same end' => false, + 'containing same start unbounded end' => false, + 'containing same end unbounded start' => false, + ], + $this->scenarioResults( + new IntegerRange(10, 20), + 'abutsEndOf', + ) + ); + // open start can't abut the end of anything + $this->assertEquals( + [ + 'same' => false, + 'unbounded' => false, + 'intersecting open start left' => false, + 'intersecting open start right' => false, + 'intersecting bounded start left' => false, + 'intersecting bounded start right' => false, + 'intersecting bounded start center' => false, + 'intersecting open end same' => false, + 'intersecting open end left' => false, + 'intersecting bounded end same' => false, + 'intersecting bounded end left' => false, + 'adjacent open end' => false, + 'adjacent bounded end' => false, + 'disjoint open end' => false, + 'disjoint bounded end' => false, + ], + $this->scenarioResults( + new IntegerRange(null, 20), + 'abutsEndOf', + ) + ); + // open end only abuts the end of things that are adjacent left + $this->assertEquals( + [ + 'same' => false, + 'unbounded' => false, + 'intersecting open end left' => false, + 'intersecting open end right' => false, + 'intersecting bounded end left' => false, + 'intersecting bounded end right' => false, + 'intersecting bounded end center' => false, + 'intersecting open start same' => false, + 'intersecting open start right' => false, + 'intersecting bounded start same' => false, + 'intersecting bounded start right' => false, + 'adjacent open start' => true, + 'adjacent bounded start' => true, + 'disjoint open start' => false, + 'disjoint bounded start' => false, + ], + $this->scenarioResults( + new IntegerRange(10, null), + 'abutsEndOf', + ) + ); + // fully unbounded can't abut anything + $this->assertEquals( + [ + 'same' => false, + 'open start' => false, + 'open end' => false, + 'bounded' => false, + ], + $this->scenarioResults( + new IntegerRange(null, null), + 'abutsEndOf', + ) + ); + } public function testAbutsStartOf() { - // fully bounded + // fully bounded only abuts the start of things that are adjacent right $this->assertEquals( [ 'same' => false, @@ -397,7 +492,7 @@ class IntegerRangeTest extends TestCase 'abutsStartOf', ) ); - // open start + // open start only abuts the start of things that are adjacent right $this->assertEquals( [ 'same' => false, @@ -421,7 +516,7 @@ class IntegerRangeTest extends TestCase 'abutsStartOf', ) ); - // open end + // open end can't abut the start of anything $this->assertEquals( [ 'same' => false, @@ -445,7 +540,7 @@ class IntegerRangeTest extends TestCase 'abutsStartOf', ) ); - // fully unbounded + // fully unbounded can't abut anything $this->assertEquals( [ 'same' => false, @@ -460,13 +555,213 @@ class IntegerRangeTest extends TestCase ); } + public function testAbuts() + { + // fully bounded only abuts things that are adjacent + $this->assertEquals( + [ + 'same' => false, + 'unbounded' => false, + 'intersecting open end left' => false, + 'intersecting open end right' => false, + 'intersecting open end center' => false, + 'intersecting open start left' => false, + 'intersecting open start right' => false, + 'intersecting open start center' => false, + 'disjoint open start' => false, + 'disjoint open end' => false, + 'disjoint left' => false, + 'disjoint right' => false, + 'adjacent open start' => true, + 'adjacent open end' => true, + 'adjacent left' => true, + 'adjacent right' => true, + 'contained' => false, + 'contained same start' => false, + 'contained same end' => false, + 'containing' => false, + 'containing unbounded start' => false, + 'containing unbounded end' => false, + 'containing same start' => false, + 'containing same end' => false, + 'containing same start unbounded end' => false, + 'containing same end unbounded start' => false, + ], + $this->scenarioResults( + new IntegerRange(10, 20), + 'abuts', + ) + ); + // open start abuts adjacent right + $this->assertEquals( + [ + 'same' => false, + 'unbounded' => false, + 'intersecting open start left' => false, + 'intersecting open start right' => false, + 'intersecting bounded start left' => false, + 'intersecting bounded start right' => false, + 'intersecting bounded start center' => false, + 'intersecting open end same' => false, + 'intersecting open end left' => false, + 'intersecting bounded end same' => false, + 'intersecting bounded end left' => false, + 'adjacent open end' => true, + 'adjacent bounded end' => true, + 'disjoint open end' => false, + 'disjoint bounded end' => false, + ], + $this->scenarioResults( + new IntegerRange(null, 20), + 'abuts', + ) + ); + // open end abuts adjacent left + $this->assertEquals( + [ + 'same' => false, + 'unbounded' => false, + 'intersecting open end left' => false, + 'intersecting open end right' => false, + 'intersecting bounded end left' => false, + 'intersecting bounded end right' => false, + 'intersecting bounded end center' => false, + 'intersecting open start same' => false, + 'intersecting open start right' => false, + 'intersecting bounded start same' => false, + 'intersecting bounded start right' => false, + 'adjacent open start' => true, + 'adjacent bounded start' => true, + 'disjoint open start' => false, + 'disjoint bounded start' => false, + ], + $this->scenarioResults( + new IntegerRange(10, null), + 'abuts', + ) + ); + // fully unbounded can't abut anything + $this->assertEquals( + [ + 'same' => false, + 'open start' => false, + 'open end' => false, + 'bounded' => false, + ], + $this->scenarioResults( + new IntegerRange(null, null), + 'abuts', + ) + ); + } + + public function testBooleanAnd() + { + // fully bounded + $this->assertEquals( + [ + 'same' => '10,20', + 'unbounded' => '10,20', + 'intersecting open end left' => '10,20', + 'intersecting open end right' => '12,20', + 'intersecting open end center' => '10,20', + 'intersecting open start left' => '10,18', + 'intersecting open start right' => '10,20', + 'intersecting open start center' => '10,20', + 'disjoint open start' => null, + 'disjoint open end' => null, + 'disjoint left' => null, + 'disjoint right' => null, + 'adjacent open start' => null, + 'adjacent open end' => null, + 'adjacent left' => null, + 'adjacent right' => null, + 'contained' => '11,19', + 'contained same start' => '10,19', + 'contained same end' => '11,20', + 'containing' => '10,20', + 'containing unbounded start' => '10,20', + 'containing unbounded end' => '10,20', + 'containing same start' => '10,20', + 'containing same end' => '10,20', + 'containing same start unbounded end' => '10,20', + 'containing same end unbounded start' => '10,20', + ], + $this->scenarioResults( + new IntegerRange(10, 20), + 'booleanAnd', + ) + ); + // open start + $this->assertEquals( + [ + 'same' => 'null,20', + 'unbounded' => 'null,20', + 'intersecting open start left' => 'null,18', + 'intersecting open start right' => 'null,20', + 'intersecting bounded start left' => '8,18', + 'intersecting bounded start right' => '12,20', + 'intersecting bounded start center' => '10,20', + 'intersecting open end same' => '20,20', + 'intersecting open end left' => '18,20', + 'intersecting bounded end same' => '20,20', + 'intersecting bounded end left' => '18,20', + 'adjacent open end' => null, + 'adjacent bounded end' => null, + 'disjoint open end' => null, + 'disjoint bounded end' => null, + ], + $this->scenarioResults( + new IntegerRange(null, 20), + 'booleanAnd', + ) + ); + // open end + $this->assertEquals( + [ + 'same' => '10,null', + 'unbounded' => '10,null', + 'intersecting open end left' => '10,null', + 'intersecting open end right' => '12,null', + 'intersecting bounded end left' => '10,18', + 'intersecting bounded end right' => '12,22', + 'intersecting bounded end center' => '10,20', + 'intersecting open start same' => '10,10', + 'intersecting open start right' => '10,12', + 'intersecting bounded start same' => '10,10', + 'intersecting bounded start right' => '10,12', + 'adjacent open start' => null, + 'adjacent bounded start' => null, + 'disjoint open start' => null, + 'disjoint bounded start' => null, + ], + $this->scenarioResults( + new IntegerRange(10, null), + 'booleanAnd', + ) + ); + // fully unbounded + $this->assertEquals( + [ + 'same' => 'null,null', + 'open start' => 'null,20', + 'open end' => '10,null', + 'bounded' => '10,20', + ], + $this->scenarioResults( + new IntegerRange(null, null), + 'booleanAnd', + ) + ); + } + protected function createScenarios(IntegerRange $range): array { if (is_null($range->start()) && is_null($range->end())) { // scenarios for fully open range return [ 'same' => new IntegerRange(null, null), - 'open start' => new IntegerRange(null, 10), + 'open start' => new IntegerRange(null, 20), 'open end' => new IntegerRange(10, null), 'bounded' => new IntegerRange(10, 20), ]; @@ -515,26 +810,26 @@ class IntegerRangeTest extends TestCase 'unbounded' => new IntegerRange(null, null), 'intersecting open end left' => new IntegerRange($range->start() - 2, null), 'intersecting open end right' => new IntegerRange($range->start() + 2, null), - 'intersecting open end center' => new IntegerRange(null, $range->end() - 2), + 'intersecting open end center' => new IntegerRange($range->start(), null), 'intersecting open start left' => new IntegerRange(null, $range->end() - 2), 'intersecting open start right' => new IntegerRange(null, $range->end() + 2), - 'intersecting open start center' => new IntegerRange($range->start() + 2, null), + 'intersecting open start center' => new IntegerRange(null, $range->end()), 'disjoint open start' => new IntegerRange(null, $range->start() - 2), 'disjoint open end' => new IntegerRange($range->end() + 2, null), - 'disjoint left' => new IntegerRange($range->start() - 10, $range->start() - 2), - 'disjoint right' => new IntegerRange($range->end() + 2, $range->end() + 10), + 'disjoint left' => new IntegerRange($range->start() - 12, $range->start() - 2), + 'disjoint right' => new IntegerRange($range->end() + 2, $range->end() + 12), 'adjacent open start' => new IntegerRange(null, $range->start() - 1), 'adjacent open end' => new IntegerRange($range->end() + 1, null), - 'adjacent left' => new IntegerRange($range->start() - 1, $range->start() - 1), - 'adjacent right' => new IntegerRange($range->end() + 1, $range->end() + 1), + 'adjacent left' => new IntegerRange($range->start() - 11, $range->start() - 1), + 'adjacent right' => new IntegerRange($range->end() + 1, $range->end() + 11), 'contained' => new IntegerRange($range->start() + 1, $range->end() - 1), 'contained same start' => new IntegerRange($range->start(), $range->end() - 1), 'contained same end' => new IntegerRange($range->start() + 1, $range->end()), - 'containing' => new IntegerRange($range->start() - 2, $range->end() + 2), - 'containing unbounded start' => new IntegerRange(null, $range->end() + 2), - 'containing unbounded end' => new IntegerRange($range->start() - 2, null), - 'containing same start' => new IntegerRange($range->start(), $range->end() + 2), - 'containing same end' => new IntegerRange($range->start() - 2, $range->end()), + 'containing' => new IntegerRange($range->start() - 1, $range->end() + 1), + 'containing unbounded start' => new IntegerRange(null, $range->end() + 1), + 'containing unbounded end' => new IntegerRange($range->start() - 1, null), + 'containing same start' => new IntegerRange($range->start(), $range->end() + 1), + 'containing same end' => new IntegerRange($range->start() - 1, $range->end()), 'containing same start unbounded end' => new IntegerRange($range->start(), null), 'containing same end unbounded start' => new IntegerRange(null, $range->end()), ]; @@ -545,7 +840,11 @@ class IntegerRangeTest extends TestCase { return array_map( function ($s) use ($range, $method) { - return $range->$method($s); + $result = $range->$method($s); + if ($result instanceof IntegerRange) { + $result = ($result->start() ?? 'null') . ',' . ($result->end() ?? 'null'); + } + return $result; }, $this->createScenarios($range) );