started range comparison tools
This commit is contained in:
parent
243ac58644
commit
51bcf5f75c
3 changed files with 781 additions and 0 deletions
196
src/Ranges/AbstractRange.php
Normal file
196
src/Ranges/AbstractRange.php
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joby's PHP Toolbox: https://code.byjoby.com/php-toolbox/
|
||||||
|
* MIT License: Copyright (c) 2024 Joby Elliott
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joby\Toolbox\Ranges;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to represent a range of values, which consists of a start and an end
|
||||||
|
* value, each of which may be null to indicate an open range in that direction.
|
||||||
|
* Any class tat extends this must implement some kind of ordering/hashing
|
||||||
|
* mechanism to convert the values you want to express into integers, so that
|
||||||
|
* they can be compared and determined to be "adjacent".
|
||||||
|
*
|
||||||
|
* For example, a range of dates could be represented by converting the date
|
||||||
|
* into a timestamp for if you wanted a resolution of 1 second, or by converting
|
||||||
|
* it to the number of days since some early epoch start if you wanted day
|
||||||
|
* resolution. The details of how things are converted mostly only matter if you
|
||||||
|
* are going to be checking for adjacency. If you don't need adjacency checks it
|
||||||
|
* only matters that your method maintains proper ordering.
|
||||||
|
*
|
||||||
|
* @template T of mixed
|
||||||
|
*/
|
||||||
|
abstract class AbstractRange
|
||||||
|
{
|
||||||
|
protected int|float $start;
|
||||||
|
protected int|float $end;
|
||||||
|
protected mixed $start_value;
|
||||||
|
protected mixed $end_value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This must be essentially a hash function, which converts a given value
|
||||||
|
* into an integer, which represents its ordering somehow.
|
||||||
|
* @param T|null $value
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
abstract protected static function convertToInt(mixed $value): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This must prepare a value to be stored in this object, which may just be
|
||||||
|
* passing it blindly, cloning an object, or rounding it, etc.
|
||||||
|
* @param T $value
|
||||||
|
* @return T
|
||||||
|
*/
|
||||||
|
abstract protected static function prepareValue(mixed $value): mixed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param T|null $start
|
||||||
|
* @param T|null $end
|
||||||
|
*/
|
||||||
|
public function __construct($start, $end)
|
||||||
|
{
|
||||||
|
$this->setStart($start);
|
||||||
|
$this->setEnd($end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param static $other
|
||||||
|
*/
|
||||||
|
public function equals(AbstractRange $other): bool
|
||||||
|
{
|
||||||
|
return $this->start == $other->start && $this->end == $other->end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param static $other
|
||||||
|
*/
|
||||||
|
public function intersects(AbstractRange $other): bool
|
||||||
|
{
|
||||||
|
if ($this->start > $other->end) return false;
|
||||||
|
if ($this->end < $other->start) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param static $other
|
||||||
|
*/
|
||||||
|
public function contains(AbstractRange $other): bool
|
||||||
|
{
|
||||||
|
if ($this->start > $other->start) return false;
|
||||||
|
if ($this->end < $other->end) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the end of this range is after the end of another range
|
||||||
|
* @param static $other
|
||||||
|
*/
|
||||||
|
public function extendsAfter(AbstractRange $other): bool
|
||||||
|
{
|
||||||
|
return $this->end > $other->end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the start of this range is before the start of another range
|
||||||
|
* @param static $other
|
||||||
|
*/
|
||||||
|
public function extendsBefore(AbstractRange $other): bool
|
||||||
|
{
|
||||||
|
return $this->start < $other->start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this range is directly adjacent to but not overlapping another range. This
|
||||||
|
* is equivalent to checking both abutsStartOf and abutsEndOf.
|
||||||
|
* @param static $other
|
||||||
|
*/
|
||||||
|
public function abuts(AbstractRange $other): bool
|
||||||
|
{
|
||||||
|
return $this->abutsEndOf($other) || $this->abutsStartOf($other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the start of this range directly abuts the end of another range.
|
||||||
|
* This means that they do not overlap, but are directly adjacent.
|
||||||
|
* @param static $other
|
||||||
|
*/
|
||||||
|
public function abutsEndOf(AbstractRange $other): bool
|
||||||
|
{
|
||||||
|
if ($this->start == -INF || $other->end == INF) return false;
|
||||||
|
return $this->start == $other->end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the end of this range directly abuts the start of another range.
|
||||||
|
* This means that they do not overlap, but are directly adjacent.
|
||||||
|
* @param static $other
|
||||||
|
*/
|
||||||
|
public function abutsStartOf(AbstractRange $other): bool
|
||||||
|
{
|
||||||
|
if ($this->end == INF || $other->start == -INF) return false;
|
||||||
|
return $this->end == $other->start - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param T|null $start
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public function setStart(mixed $start): static
|
||||||
|
{
|
||||||
|
$this->start = is_null($start) ? -INF
|
||||||
|
: static::convertToInt($start);
|
||||||
|
$this->start_value = is_null($start) ? null
|
||||||
|
: static::prepareValue($start);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param T|null $end
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public function setEnd(mixed $end): static
|
||||||
|
{
|
||||||
|
$this->end = is_null($end) ? INF
|
||||||
|
: static::convertToInt($end);
|
||||||
|
$this->end_value = is_null($end) ? null
|
||||||
|
: static::prepareValue($end);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return T|null
|
||||||
|
*/
|
||||||
|
public function start(): mixed
|
||||||
|
{
|
||||||
|
return $this->start_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return T|null
|
||||||
|
*/
|
||||||
|
public function end(): mixed
|
||||||
|
{
|
||||||
|
return $this->end_value;
|
||||||
|
}
|
||||||
|
}
|
47
src/Ranges/IntegerRange.php
Normal file
47
src/Ranges/IntegerRange.php
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joby's PHP Toolbox: https://code.byjoby.com/php-toolbox/
|
||||||
|
* MIT License: Copyright (c) 2024 Joby Elliott
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joby\Toolbox\Ranges;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The simplest possible implementation of AbstractRange, which uses integers as
|
||||||
|
* its values as well as it's internal hashes. All it does to convert values is
|
||||||
|
* cast them to integers.
|
||||||
|
*
|
||||||
|
* @extends AbstractRange<int>
|
||||||
|
*/
|
||||||
|
class IntegerRange extends AbstractRange
|
||||||
|
{
|
||||||
|
|
||||||
|
protected static function convertToInt(mixed $value): int
|
||||||
|
{
|
||||||
|
return (int)$value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function prepareValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
return (int)$value;
|
||||||
|
}
|
||||||
|
}
|
538
tests/Ranges/IntegerRangeTest.php
Normal file
538
tests/Ranges/IntegerRangeTest.php
Normal file
|
@ -0,0 +1,538 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joby's PHP Toolbox: https://code.byjoby.com/php-toolbox/
|
||||||
|
* MIT License: Copyright (c) 2024 Joby Elliott
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joby\Toolbox\Sorting;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Joby\Toolbox\Ranges\IntegerRange;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class IntegerRangeTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testInstantiation()
|
||||||
|
{
|
||||||
|
// test fully bounded
|
||||||
|
$range = new IntegerRange(1, 10);
|
||||||
|
$this->assertInstanceOf(IntegerRange::class, $range);
|
||||||
|
$this->assertEquals(1, $range->start());
|
||||||
|
$this->assertEquals(10, $range->end());
|
||||||
|
// test open start
|
||||||
|
$range = new IntegerRange(null, 10);
|
||||||
|
$this->assertInstanceOf(IntegerRange::class, $range);
|
||||||
|
$this->assertNull($range->start());
|
||||||
|
$this->assertEquals(10, $range->end());
|
||||||
|
// test open end
|
||||||
|
$range = new IntegerRange(1, null);
|
||||||
|
$this->assertInstanceOf(IntegerRange::class, $range);
|
||||||
|
$this->assertEquals(1, $range->start());
|
||||||
|
$this->assertNull($range->end());
|
||||||
|
// test open both
|
||||||
|
$range = new IntegerRange(null, null);
|
||||||
|
$this->assertInstanceOf(IntegerRange::class, $range);
|
||||||
|
$this->assertNull($range->start());
|
||||||
|
$this->assertNull($range->end());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEquals()
|
||||||
|
{
|
||||||
|
// fully bounded
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(10, 20),
|
||||||
|
'equals',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'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' => false,
|
||||||
|
'adjacent open end' => false,
|
||||||
|
'adjacent left' => false,
|
||||||
|
'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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// open start
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(null, 20),
|
||||||
|
'equals',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// open end
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(10, null),
|
||||||
|
'equals',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'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' => false,
|
||||||
|
'adjacent bounded start' => false,
|
||||||
|
'disjoint open start' => false,
|
||||||
|
'disjoint bounded start' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// fully unbounded
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(null, null),
|
||||||
|
'equals',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'open start' => false,
|
||||||
|
'open end' => false,
|
||||||
|
'bounded' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIntersects()
|
||||||
|
{
|
||||||
|
// fully bounded
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(10, 20),
|
||||||
|
'intersects',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'unbounded' => true,
|
||||||
|
'intersecting open end left' => true,
|
||||||
|
'intersecting open end right' => true,
|
||||||
|
'intersecting open end center' => true,
|
||||||
|
'intersecting open start left' => true,
|
||||||
|
'intersecting open start right' => true,
|
||||||
|
'intersecting open start center' => true,
|
||||||
|
'disjoint open start' => false,
|
||||||
|
'disjoint open end' => false,
|
||||||
|
'disjoint left' => false,
|
||||||
|
'disjoint right' => false,
|
||||||
|
'adjacent open start' => false,
|
||||||
|
'adjacent open end' => false,
|
||||||
|
'adjacent left' => false,
|
||||||
|
'adjacent right' => false,
|
||||||
|
'contained' => true,
|
||||||
|
'contained same start' => true,
|
||||||
|
'contained same end' => true,
|
||||||
|
'containing' => true,
|
||||||
|
'containing unbounded start' => true,
|
||||||
|
'containing unbounded end' => true,
|
||||||
|
'containing same start' => true,
|
||||||
|
'containing same end' => true,
|
||||||
|
'containing same start unbounded end' => true,
|
||||||
|
'containing same end unbounded start' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// open start
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(null, 20),
|
||||||
|
'intersects',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'unbounded' => true,
|
||||||
|
'intersecting open start left' => true,
|
||||||
|
'intersecting open start right' => true,
|
||||||
|
'intersecting bounded start left' => true,
|
||||||
|
'intersecting bounded start right' => true,
|
||||||
|
'intersecting bounded start center' => true,
|
||||||
|
'intersecting open end same' => true,
|
||||||
|
'intersecting open end left' => true,
|
||||||
|
'intersecting bounded end same' => true,
|
||||||
|
'intersecting bounded end left' => true,
|
||||||
|
'adjacent open end' => false,
|
||||||
|
'adjacent bounded end' => false,
|
||||||
|
'disjoint open end' => false,
|
||||||
|
'disjoint bounded end' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// open end
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(10, null),
|
||||||
|
'intersects',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'unbounded' => true,
|
||||||
|
'intersecting open end left' => true,
|
||||||
|
'intersecting open end right' => true,
|
||||||
|
'intersecting bounded end left' => true,
|
||||||
|
'intersecting bounded end right' => true,
|
||||||
|
'intersecting bounded end center' => true,
|
||||||
|
'intersecting open start same' => true,
|
||||||
|
'intersecting open start right' => true,
|
||||||
|
'intersecting bounded start same' => true,
|
||||||
|
'intersecting bounded start right' => true,
|
||||||
|
'adjacent open start' => false,
|
||||||
|
'adjacent bounded start' => false,
|
||||||
|
'disjoint open start' => false,
|
||||||
|
'disjoint bounded start' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// fully unbounded
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(null, null),
|
||||||
|
'intersects',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'open start' => true,
|
||||||
|
'open end' => true,
|
||||||
|
'bounded' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testContains()
|
||||||
|
{
|
||||||
|
// fully bounded
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(10, 20),
|
||||||
|
'contains',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'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' => false,
|
||||||
|
'adjacent open end' => false,
|
||||||
|
'adjacent left' => false,
|
||||||
|
'adjacent right' => false,
|
||||||
|
'contained' => true,
|
||||||
|
'contained same start' => true,
|
||||||
|
'contained same end' => true,
|
||||||
|
'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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// open start
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(null, 20),
|
||||||
|
'contains',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'unbounded' => false,
|
||||||
|
'intersecting open start left' => true,
|
||||||
|
'intersecting open start right' => false,
|
||||||
|
'intersecting bounded start left' => true,
|
||||||
|
'intersecting bounded start right' => false,
|
||||||
|
'intersecting bounded start center' => true,
|
||||||
|
'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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// open end
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(10, null),
|
||||||
|
'contains',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'unbounded' => false,
|
||||||
|
'intersecting open end left' => false,
|
||||||
|
'intersecting open end right' => true,
|
||||||
|
'intersecting bounded end left' => false,
|
||||||
|
'intersecting bounded end right' => true,
|
||||||
|
'intersecting bounded end center' => true,
|
||||||
|
'intersecting open start same' => false,
|
||||||
|
'intersecting open start right' => false,
|
||||||
|
'intersecting bounded start same' => false,
|
||||||
|
'intersecting bounded start right' => false,
|
||||||
|
'adjacent open start' => false,
|
||||||
|
'adjacent bounded start' => false,
|
||||||
|
'disjoint open start' => false,
|
||||||
|
'disjoint bounded start' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// fully unbounded
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(null, null),
|
||||||
|
'contains',
|
||||||
|
[
|
||||||
|
'same' => true,
|
||||||
|
'open start' => true,
|
||||||
|
'open end' => true,
|
||||||
|
'bounded' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: testAbutsEndOf
|
||||||
|
|
||||||
|
// TODO: see about making failures go to the right line/test
|
||||||
|
|
||||||
|
public function testAbutsStartOf()
|
||||||
|
{
|
||||||
|
// fully bounded
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(10, 20),
|
||||||
|
'abutsStartOf',
|
||||||
|
[
|
||||||
|
'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' => false,
|
||||||
|
'adjacent open end' => true,
|
||||||
|
'adjacent left' => false,
|
||||||
|
'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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// open start
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(null, 20),
|
||||||
|
'abutsStartOf',
|
||||||
|
[
|
||||||
|
'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,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// open end
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(10, null),
|
||||||
|
'abutsStartOf',
|
||||||
|
[
|
||||||
|
'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' => false,
|
||||||
|
'adjacent bounded start' => false,
|
||||||
|
'disjoint open start' => false,
|
||||||
|
'disjoint bounded start' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// fully unbounded
|
||||||
|
$this->checkScenarioResults(
|
||||||
|
new IntegerRange(null, null),
|
||||||
|
'abutsStartOf',
|
||||||
|
[
|
||||||
|
'same' => false,
|
||||||
|
'open start' => false,
|
||||||
|
'open end' => false,
|
||||||
|
'bounded' => false,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 end' => new IntegerRange(10, null),
|
||||||
|
'bounded' => new IntegerRange(10, 20),
|
||||||
|
];
|
||||||
|
} elseif (is_null($range->start())) {
|
||||||
|
// scenarios for unbounded start
|
||||||
|
return [
|
||||||
|
'same' => new IntegerRange(null, $range->end()),
|
||||||
|
'unbounded' => new IntegerRange(null, null),
|
||||||
|
'intersecting open start left' => new IntegerRange(null, $range->end() - 2),
|
||||||
|
'intersecting open start right' => new IntegerRange(null, $range->end() + 2),
|
||||||
|
'intersecting bounded start left' => new IntegerRange($range->end() - 12, $range->end() - 2),
|
||||||
|
'intersecting bounded start right' => new IntegerRange($range->end() - 8, $range->end() + 2),
|
||||||
|
'intersecting bounded start center' => new IntegerRange($range->end() - 10, $range->end()),
|
||||||
|
'intersecting open end same' => new IntegerRange($range->end(), null),
|
||||||
|
'intersecting open end left' => new IntegerRange($range->end() - 2, null),
|
||||||
|
'intersecting bounded end same' => new IntegerRange($range->end(), $range->end() + 10),
|
||||||
|
'intersecting bounded end left' => new IntegerRange($range->end() - 2, $range->end() + 10),
|
||||||
|
'adjacent open end' => new IntegerRange($range->end() + 1, null),
|
||||||
|
'adjacent bounded end' => new IntegerRange($range->end() + 1, $range->end() + 10),
|
||||||
|
'disjoint open end' => new IntegerRange($range->end() + 2, null),
|
||||||
|
'disjoint bounded end' => new IntegerRange($range->end() + 2, $range->end() + 12),
|
||||||
|
];
|
||||||
|
} elseif (is_null($range->end())) {
|
||||||
|
// scenarios for unbounded end
|
||||||
|
return [
|
||||||
|
'same' => new IntegerRange($range->start(), null),
|
||||||
|
'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 bounded end left' => new IntegerRange($range->start() - 2, $range->start() + 8),
|
||||||
|
'intersecting bounded end right' => new IntegerRange($range->start() + 2, $range->start() + 12),
|
||||||
|
'intersecting bounded end center' => new IntegerRange($range->start(), $range->start() + 10),
|
||||||
|
'intersecting open start same' => new IntegerRange(null, $range->start()),
|
||||||
|
'intersecting open start right' => new IntegerRange(null, $range->start() + 2),
|
||||||
|
'intersecting bounded start same' => new IntegerRange($range->start() - 10, $range->start()),
|
||||||
|
'intersecting bounded start right' => new IntegerRange($range->start() - 12, $range->start() + 2),
|
||||||
|
'adjacent open start' => new IntegerRange(null, $range->start() - 1),
|
||||||
|
'adjacent bounded start' => new IntegerRange($range->start() - 10, $range->start() - 1),
|
||||||
|
'disjoint open start' => new IntegerRange(null, $range->start() - 2),
|
||||||
|
'disjoint bounded start' => new IntegerRange($range->start() - 12, $range->start() - 2),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// scenarios for fully bounded range
|
||||||
|
return [
|
||||||
|
'same' => new IntegerRange($range->start(), $range->end()),
|
||||||
|
'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 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),
|
||||||
|
'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),
|
||||||
|
'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),
|
||||||
|
'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 same start unbounded end' => new IntegerRange($range->start(), null),
|
||||||
|
'containing same end unbounded start' => new IntegerRange(null, $range->end()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkScenarioResults(IntegerRange $range, string $method, array $expected_results)
|
||||||
|
{
|
||||||
|
$scenarios = $this->createScenarios($range);
|
||||||
|
foreach ($scenarios as $scenario => $other) {
|
||||||
|
$result = $range->$method($other);
|
||||||
|
if (!isset($expected_results[$scenario])) {
|
||||||
|
throw new Exception("No expected result for scenario '$scenario'");
|
||||||
|
}
|
||||||
|
$expected = $expected_results[$scenario];
|
||||||
|
unset($expected_results[$scenario]);
|
||||||
|
$this->assertEquals($expected, $result, sprintf(
|
||||||
|
"[%s,%s] %s [%s,%s] expected %s but returned %s (scenario %s)",
|
||||||
|
$range->start() ?? '-INF',
|
||||||
|
$range->end() ?? 'INF',
|
||||||
|
$method,
|
||||||
|
$other->start() ?? '-INF',
|
||||||
|
$other->end() ?? 'INF',
|
||||||
|
$expected ? 'true' : 'false',
|
||||||
|
$result ? 'true' : 'false',
|
||||||
|
$scenario
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (count($expected_results) > 0) {
|
||||||
|
throw new Exception("Expected results not used: " . implode(', ', array_keys($expected_results)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue