From e7d6aa07fa4cd6f89bce62198091266ca4fe289d Mon Sep 17 00:00:00 2001 From: Joby Elliott Date: Tue, 30 Jul 2024 09:59:25 -0600 Subject: [PATCH] refactored to use collections instead of arrays for results --- src/Ranges/AbstractRange.php | 94 +++--- src/Ranges/IntegerRange.php | 13 +- src/Ranges/RangeCollection.php | 166 ++++++++++ src/Sorting/Sorter.php | 6 +- tests/Ranges/IntegerRangeTest.php | 532 +++++++++++++++--------------- 5 files changed, 495 insertions(+), 316 deletions(-) create mode 100644 src/Ranges/RangeCollection.php diff --git a/src/Ranges/AbstractRange.php b/src/Ranges/AbstractRange.php index 87b9649..d393956 100644 --- a/src/Ranges/AbstractRange.php +++ b/src/Ranges/AbstractRange.php @@ -131,22 +131,28 @@ abstract class AbstractRange * overlap this array will contain both ranges separately). Separate objects * must be returned in ascending order. * @param static $other - * @return static[] + * @return RangeCollection */ - public function booleanOr(AbstractRange $other): array + public function booleanOr(AbstractRange $other): RangeCollection { if ($this->intersects($other) || $this->adjacent($other)) { - return [ + return RangeCollection::create( new static( $this->extendsBefore($other) ? $this->start() : $other->start(), $this->extendsAfter($other) ? $this->end() : $other->end() ) - ]; + ); } else { if ($this->extendsBefore($other)) { - return [new static($this->start(), $this->end()), new static($other->start(), $other->end())]; + return RangeCollection::create( + new static($this->start(), $this->end()), + new static($other->start(), $other->end()) + ); } else { - return [new static($other->start(), $other->end()), new static($this->start(), $this->end())]; + return RangeCollection::create( + new static($other->start(), $other->end()), + new static($this->start(), $this->end()) + ); } } } @@ -157,22 +163,17 @@ abstract class AbstractRange * ranges do not overlap, this array will contain both ranges separately. * Separate objects must be returned in ascending order. * @param static $other - * @return static[] + * @return RangeCollection */ - public function booleanXor(AbstractRange $other): array + public function booleanXor(AbstractRange $other): RangeCollection { // if the ranges are equal, return an empty array - if ($this->equals($other)) return []; + if ($this->equals($other)) return RangeCollection::createEmpty($other); // if the ranges are adjacent return a single range - if ($this->adjacentLeftOf($other)) return [new static($this->start(), $other->end())]; - if ($this->adjacentRightOf($other)) return [new static($other->start(), $this->end())]; + if ($this->adjacent($other)) return $this->booleanOr($other); // if the ranges do not overlap, return both ranges if (!$this->intersects($other)) { - if ($this->extendsBefore($other)) { - return [new static($this->start(), $this->end()), new static($other->start(), $other->end())]; - } else { - return [new static($other->start(), $other->end()), new static($this->start(), $this->end())]; - } + return RangeCollection::create(new static($this->start(), $this->end()), new static($other->start(), $other->end())); } // otherwise get the maximum bounds minus wherever these intersect $range = new static( @@ -182,7 +183,7 @@ abstract class AbstractRange if ($intersect = $this->booleanAnd($other)) { return $range->booleanNot($intersect); } else { - return [$range]; + return RangeCollection::create($range); } } @@ -196,22 +197,18 @@ abstract class AbstractRange * - 2 ranges: the entered ranges are adjacent, disjoint, or overlap with a shared boundary * - 3 ranges: the entered ranges overlap with space on each end * @param static $other - * @return static[] + * @return RangeCollection */ - public function booleanSlice(AbstractRange $other): array + public function booleanSlice(AbstractRange $other): RangeCollection { // if the ranges are equal, return a single range - if ($this->equals($other)) return [new static($this->start(), $this->end())]; - // if the ranges are adjacent, return two ranges - if ($this->adjacentLeftOf($other)) return [new static($this->start(), $this->end()), new static($other->start(), $other->end())]; - if ($this->adjacentRightOf($other)) return [new static($other->start(), $other->end()), new static($this->start(), $this->end())]; + if ($this->equals($other)) return RangeCollection::create(new static($this->start(), $this->end())); // if the ranges do not overlap, return two ranges if (!$this->intersects($other)) { - if ($this->extendsBefore($other)) { - return [new static($this->start(), $this->end()), new static($other->start(), $other->end())]; - } else { - return [new static($other->start(), $other->end()), new static($this->start(), $this->end())]; - } + return RangeCollection::create( + new static($this->start(), $this->end()), + new static($other->start(), $other->end()) + ); } // otherwise get the maximum bounds minus wherever these intersect $overall_range = new static( @@ -219,15 +216,14 @@ abstract class AbstractRange $this->extendsAfter($other) ? $this->end() : $other->end() ); $intersection = $this->booleanAnd($other); + assert($intersection !== null); $xor = $overall_range->booleanNot($intersection); if (count($xor) == 2) { - return [$xor[0], $intersection, $xor[1]]; + assert(isset($xor[0], $xor[1])); + return RangeCollection::create($xor[0], $intersection, $xor[1]); } elseif (count($xor) == 1) { - if ($intersection->extendsBefore($xor[0])) { - return [$intersection, $xor[0]]; - } else { - return [$xor[0], $intersection]; - } + assert(isset($xor[0])); + return RangeCollection::create($intersection, $xor[0]); } // throw an exception if we get in an unexpected state throw new RuntimeException(sprintf("Unexpected state (%s,%s) (%s,%s)", $this->start, $this->end, $other->start, $other->end)); @@ -239,38 +235,38 @@ abstract class AbstractRange * the other range completely covers this range, an empty array will be * returned. Separate objects must be returned in ascending order. * @param static $other - * @return static[] + * @return RangeCollection */ - public function booleanNot(AbstractRange $other): array + public function booleanNot(AbstractRange $other): RangeCollection { // if this range is completely contained by the other, return an empty array if ($other->contains($this)) { - return []; + return RangeCollection::createEmpty($other); } // if the ranges do not overlap, return this range if (!$this->intersects($other)) { - return [new static($this->start(), $this->end())]; + return RangeCollection::create(new static($this->start(), $this->end())); } // if this range completely contains the other, return the range from the start of this range to the start of the other if ($this->contains($other)) { if ($this->start == $other->start) { - return [new static(static::valueAfter($other->end), $this->end())]; + return RangeCollection::create(new static(static::valueAfter($other->end), $this->end())); } elseif ($this->end == $other->end) { - return [new static($this->start(), static::valueBefore($other->start))]; + return RangeCollection::create(new static($this->start(), static::valueBefore($other->start))); } else { - return [ + return RangeCollection::create( new static($this->start(), static::valueBefore($other->start)), new static(static::valueAfter($other->end), $this->end()) - ]; + ); } } // if this range extends before the other, return the range from the start of this range to the start of the other if ($this->extendsBefore($other)) { - return [new static($this->start(), static::valueBefore($other->start))]; + return RangeCollection::create(new static($this->start(), static::valueBefore($other->start))); } // if this range extends after the other, return the range from the end of the other to the end of this range if ($this->extendsAfter($other)) { - return [new static(static::valueAfter($other->end), $this->end())]; + return RangeCollection::create(new static(static::valueAfter($other->end), $this->end())); } // throw an exception if we get in an unexpected state throw new RuntimeException(sprintf("Unexpected state (%s,%s) (%s,%s)", $this->start, $this->end, $other->start, $other->end)); @@ -398,4 +394,14 @@ abstract class AbstractRange { return $this->end_value; } + + public function startAsNumber(): int|float + { + return $this->start; + } + + public function endAsNumber(): int|float + { + return $this->end; + } } diff --git a/src/Ranges/IntegerRange.php b/src/Ranges/IntegerRange.php index 43cfae0..02f6df2 100644 --- a/src/Ranges/IntegerRange.php +++ b/src/Ranges/IntegerRange.php @@ -25,6 +25,8 @@ namespace Joby\Toolbox\Ranges; +use Stringable; + /** * 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 @@ -36,7 +38,7 @@ namespace Joby\Toolbox\Ranges; * * @extends AbstractRange */ -class IntegerRange extends AbstractRange +class IntegerRange extends AbstractRange implements Stringable { protected static function valueToInteger(mixed $value): int { @@ -52,4 +54,13 @@ class IntegerRange extends AbstractRange { return (int)$value; } + + public function __toString(): string + { + return sprintf( + '[%s...%s]', + $this->start === -INF ? '' : $this->start, + $this->end === INF ? '' : $this->end + ); + } } diff --git a/src/Ranges/RangeCollection.php b/src/Ranges/RangeCollection.php new file mode 100644 index 0000000..22aac8a --- /dev/null +++ b/src/Ranges/RangeCollection.php @@ -0,0 +1,166 @@ + + * @implements IteratorAggregate + */ +class RangeCollection implements Countable, ArrayAccess, IteratorAggregate +{ + protected string $class; + /** @var T[] */ + protected $ranges = []; + + /** + * @template RangeType of AbstractRange + * @param RangeType $range + * @param RangeType ...$ranges + * @return RangeCollection + */ + public static function create(AbstractRange $range, AbstractRange ...$ranges): RangeCollection + { + return new RangeCollection($range::class, $range, ...$ranges); + } + + /** + * @template RangeType of AbstractRange + * @param RangeType|class-string $class + * @return RangeCollection + */ + public static function createEmpty(AbstractRange|string $class): RangeCollection + { + if (is_object($class)) return new RangeCollection($class::class); + else return new RangeCollection($class); + } + + /** + * @return T[] + */ + public function toArray(): array + { + return $this->ranges; + } + + /** + * @param T ...$ranges + */ + public function add(AbstractRange ...$ranges): static + { + foreach ($ranges as $range) { + if (!($range instanceof $this->class)) { + throw new InvalidArgumentException("Ranges must be of type $this->class"); + } + } + $this->ranges = array_merge($this->ranges, $ranges); + $this->sort(); + return $this; + } + + /** + * @param class-string $class + * @param T ...$ranges + * @return void + */ + protected function __construct(string $class, AbstractRange ...$ranges) + { + $this->class = $class; + $this->add(...$ranges); + } + + protected function sort(): void + { + static $sorter; + $sorter = $sorter ?? $sorter = new Sorter( + fn (AbstractRange $a, AbstractRange $b): int => $a->startAsNumber() <=> $b->startAsNumber(), + fn (AbstractRange $a, AbstractRange $b): int => $a->endAsNumber() <=> $b->endAsNumber(), + ); + $sorter->sort($this->ranges); + } + + public function count(): int + { + return count($this->ranges); + } + + /** + * @param int $offset + * @return bool + */ + public function offsetExists($offset): bool + { + return isset($this->ranges[$offset]); + } + + /** + * @param int $offset + * @return T|null + */ + public function offsetGet($offset): ?AbstractRange + { + return $this->ranges[$offset] ?? null; + } + + /** + * @param int|null $offset + * @param T $value + */ + public function offsetSet($offset, $value): void + { + if (is_null($offset)) $this->add($value); + else { + if (!($value instanceof $this->class)) { + throw new InvalidArgumentException("Ranges must be of type $this->class"); + } + $this->ranges[$offset] = $value; + $this->sort(); + } + } + + /** + * @param int $offset + */ + public function offsetUnset($offset): void + { + unset($this->ranges[$offset]); + } + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->ranges); + } +} diff --git a/src/Sorting/Sorter.php b/src/Sorting/Sorter.php index 30efa39..2f871d6 100644 --- a/src/Sorting/Sorter.php +++ b/src/Sorting/Sorter.php @@ -37,7 +37,7 @@ namespace Joby\Toolbox\Sorting; */ class Sorter { - /** @var array */ + /** @var array */ protected array $comparisons = []; /** @@ -45,7 +45,7 @@ class Sorter * The sorters will be called in order, and the array will be sorted based * on the first one to return a non-zero value. * - * @param callable(mixed, mixed): int ...$comparisons + * @param callable ...$comparisons */ public function __construct(callable ...$comparisons) { @@ -56,7 +56,7 @@ class Sorter * Add one or more sorting callbacks to this sorter. The new callbacks will * be appended to the end of the existing list of sorters. * - * @param callable(mixed, mixed): int ...$comparisons + * @param callable ...$comparisons */ public function addComparison(callable ...$comparisons): static { diff --git a/tests/Ranges/IntegerRangeTest.php b/tests/Ranges/IntegerRangeTest.php index 66dbeb6..c3f0c41 100644 --- a/tests/Ranges/IntegerRangeTest.php +++ b/tests/Ranges/IntegerRangeTest.php @@ -26,6 +26,7 @@ namespace Joby\Toolbox\Sorting; use Joby\Toolbox\Ranges\IntegerRange; +use Joby\Toolbox\Ranges\RangeCollection; use PHPUnit\Framework\TestCase; class IntegerRangeTest extends TestCase @@ -660,14 +661,14 @@ class IntegerRangeTest extends TestCase // fully bounded $this->assertEquals( [ - 'same' => '10,20', - 'unbounded' => '10,20', - 'intersecting open end left' => '10,20', - 'intersecting open end right' => '11,20', - 'intersecting open end center' => '10,20', - 'intersecting open start left' => '10,19', - 'intersecting open start right' => '10,20', - 'intersecting open start center' => '10,20', + 'same' => '[10...20]', + 'unbounded' => '[10...20]', + 'intersecting open end left' => '[10...20]', + 'intersecting open end right' => '[11...20]', + 'intersecting open end center' => '[10...20]', + 'intersecting open start left' => '[10...19]', + 'intersecting open start right' => '[10...20]', + 'intersecting open start center' => '[10...20]', 'disjoint open start' => null, 'disjoint open end' => null, 'disjoint left' => null, @@ -676,16 +677,16 @@ class IntegerRangeTest extends TestCase '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', + '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), @@ -695,17 +696,17 @@ class IntegerRangeTest extends TestCase // open start $this->assertEquals( [ - 'same' => 'null,20', - 'unbounded' => 'null,20', - 'intersecting open start left' => 'null,19', - 'intersecting open start right' => 'null,20', - 'intersecting bounded start left' => '9,19', - 'intersecting bounded start right' => '11,20', - 'intersecting bounded start center' => '10,20', - 'intersecting open end same' => '20,20', - 'intersecting open end left' => '19,20', - 'intersecting bounded end same' => '20,20', - 'intersecting bounded end left' => '19,20', + 'same' => '[...20]', + 'unbounded' => '[...20]', + 'intersecting open start left' => '[...19]', + 'intersecting open start right' => '[...20]', + 'intersecting bounded start left' => '[9...19]', + 'intersecting bounded start right' => '[11...20]', + 'intersecting bounded start center' => '[10...20]', + 'intersecting open end same' => '[20...20]', + 'intersecting open end left' => '[19...20]', + 'intersecting bounded end same' => '[20...20]', + 'intersecting bounded end left' => '[19...20]', 'adjacent open end' => null, 'adjacent bounded end' => null, 'disjoint open end' => null, @@ -719,17 +720,17 @@ class IntegerRangeTest extends TestCase // open end $this->assertEquals( [ - 'same' => '10,null', - 'unbounded' => '10,null', - 'intersecting open end left' => '10,null', - 'intersecting open end right' => '11,null', - 'intersecting bounded end left' => '10,19', - 'intersecting bounded end right' => '11,21', - 'intersecting bounded end center' => '10,20', - 'intersecting open start same' => '10,10', - 'intersecting open start right' => '10,11', - 'intersecting bounded start same' => '10,10', - 'intersecting bounded start right' => '10,11', + 'same' => '[10...]', + 'unbounded' => '[10...]', + 'intersecting open end left' => '[10...]', + 'intersecting open end right' => '[11...]', + 'intersecting bounded end left' => '[10...19]', + 'intersecting bounded end right' => '[11...21]', + 'intersecting bounded end center' => '[10...20]', + 'intersecting open start same' => '[10...10]', + 'intersecting open start right' => '[10...11]', + 'intersecting bounded start same' => '[10...10]', + 'intersecting bounded start right' => '[10...11]', 'adjacent open start' => null, 'adjacent bounded start' => null, 'disjoint open start' => null, @@ -743,10 +744,10 @@ class IntegerRangeTest extends TestCase // fully unbounded $this->assertEquals( [ - 'same' => 'null,null', - 'open start' => 'null,20', - 'open end' => '10,null', - 'bounded' => '10,20', + 'same' => '[...]', + 'open start' => '[...20]', + 'open end' => '[10...]', + 'bounded' => '[10...20]', ], $this->scenarioResults( new IntegerRange(null, null), @@ -760,32 +761,32 @@ class IntegerRangeTest extends TestCase // fully bounded $this->assertEquals( [ - 'same' => '10,20', - 'unbounded' => 'null,null', - 'intersecting open end left' => '9,null', - 'intersecting open end right' => '10,null', - 'intersecting open end center' => '10,null', - 'intersecting open start left' => 'null,20', - 'intersecting open start right' => 'null,21', - 'intersecting open start center' => 'null,20', - 'disjoint open start' => 'null,8;10,20', - 'disjoint open end' => '10,20;22,null', - 'disjoint left' => '-2,8;10,20', - 'disjoint right' => '10,20;22,32', - 'adjacent open start' => 'null,20', - 'adjacent open end' => '10,null', - 'adjacent left' => '-1,20', - 'adjacent right' => '10,31', - 'contained' => '10,20', - 'contained same start' => '10,20', - 'contained same end' => '10,20', - 'containing' => '9,21', - 'containing unbounded start' => 'null,21', - 'containing unbounded end' => '9,null', - 'containing same start' => '10,21', - 'containing same end' => '9,20', - 'containing same start unbounded end' => '10,null', - 'containing same end unbounded start' => 'null,20', + 'same' => '[10...20]', + 'unbounded' => '[...]', + 'intersecting open end left' => '[9...]', + 'intersecting open end right' => '[10...]', + 'intersecting open end center' => '[10...]', + 'intersecting open start left' => '[...20]', + 'intersecting open start right' => '[...21]', + 'intersecting open start center' => '[...20]', + 'disjoint open start' => '[...8] [10...20]', + 'disjoint open end' => '[10...20] [22...]', + 'disjoint left' => '[-2...8] [10...20]', + 'disjoint right' => '[10...20] [22...32]', + 'adjacent open start' => '[...20]', + 'adjacent open end' => '[10...]', + 'adjacent left' => '[-1...20]', + 'adjacent right' => '[10...31]', + 'contained' => '[10...20]', + 'contained same start' => '[10...20]', + 'contained same end' => '[10...20]', + 'containing' => '[9...21]', + 'containing unbounded start' => '[...21]', + 'containing unbounded end' => '[9...]', + 'containing same start' => '[10...21]', + 'containing same end' => '[9...20]', + 'containing same start unbounded end' => '[10...]', + 'containing same end unbounded start' => '[...20]', ], $this->scenarioResults( new IntegerRange(10, 20), @@ -795,21 +796,21 @@ class IntegerRangeTest extends TestCase // open start $this->assertEquals( [ - 'same' => 'null,20', - 'unbounded' => 'null,null', - 'intersecting open start left' => 'null,20', - 'intersecting open start right' => 'null,21', - 'intersecting bounded start left' => 'null,20', - 'intersecting bounded start right' => 'null,21', - 'intersecting bounded start center' => 'null,20', - 'intersecting open end same' => 'null,null', - 'intersecting open end left' => 'null,null', - 'intersecting bounded end same' => 'null,30', - 'intersecting bounded end left' => 'null,29', - 'adjacent open end' => 'null,null', - 'adjacent bounded end' => 'null,30', - 'disjoint open end' => 'null,20;22,null', - 'disjoint bounded end' => 'null,20;22,32', + 'same' => '[...20]', + 'unbounded' => '[...]', + 'intersecting open start left' => '[...20]', + 'intersecting open start right' => '[...21]', + 'intersecting bounded start left' => '[...20]', + 'intersecting bounded start right' => '[...21]', + 'intersecting bounded start center' => '[...20]', + 'intersecting open end same' => '[...]', + 'intersecting open end left' => '[...]', + 'intersecting bounded end same' => '[...30]', + 'intersecting bounded end left' => '[...29]', + 'adjacent open end' => '[...]', + 'adjacent bounded end' => '[...30]', + 'disjoint open end' => '[...20] [22...]', + 'disjoint bounded end' => '[...20] [22...32]', ], $this->scenarioResults( new IntegerRange(null, 20), @@ -819,21 +820,21 @@ class IntegerRangeTest extends TestCase // open end $this->assertEquals( [ - 'same' => '10,null', - 'unbounded' => 'null,null', - 'intersecting open end left' => '9,null', - 'intersecting open end right' => '10,null', - 'intersecting bounded end left' => '9,null', - 'intersecting bounded end right' => '10,null', - 'intersecting bounded end center' => '10,null', - 'intersecting open start same' => 'null,null', - 'intersecting open start right' => 'null,null', - 'intersecting bounded start same' => '0,null', - 'intersecting bounded start right' => '1,null', - 'adjacent open start' => 'null,null', - 'adjacent bounded start' => '0,null', - 'disjoint open start' => 'null,8;10,null', - 'disjoint bounded start' => '-2,8;10,null', + 'same' => '[10...]', + 'unbounded' => '[...]', + 'intersecting open end left' => '[9...]', + 'intersecting open end right' => '[10...]', + 'intersecting bounded end left' => '[9...]', + 'intersecting bounded end right' => '[10...]', + 'intersecting bounded end center' => '[10...]', + 'intersecting open start same' => '[...]', + 'intersecting open start right' => '[...]', + 'intersecting bounded start same' => '[0...]', + 'intersecting bounded start right' => '[1...]', + 'adjacent open start' => '[...]', + 'adjacent bounded start' => '[0...]', + 'disjoint open start' => '[...8] [10...]', + 'disjoint bounded start' => '[-2...8] [10...]', ], $this->scenarioResults( new IntegerRange(10, null), @@ -843,10 +844,10 @@ class IntegerRangeTest extends TestCase // fully unbounded $this->assertEquals( [ - 'same' => 'null,null', - 'open start' => 'null,null', - 'open end' => 'null,null', - 'bounded' => 'null,null', + 'same' => '[...]', + 'open start' => '[...]', + 'open end' => '[...]', + 'bounded' => '[...]', ], $this->scenarioResults( new IntegerRange(null, null), @@ -863,22 +864,22 @@ class IntegerRangeTest extends TestCase 'same' => '', 'unbounded' => '', 'intersecting open end left' => '', - 'intersecting open end right' => '10,10', + 'intersecting open end right' => '[10...10]', 'intersecting open end center' => '', - 'intersecting open start left' => '20,20', + 'intersecting open start left' => '[20...20]', 'intersecting open start right' => '', 'intersecting open start center' => '', - 'disjoint open start' => '10,20', - 'disjoint open end' => '10,20', - 'disjoint left' => '10,20', - 'disjoint right' => '10,20', - 'adjacent open start' => '10,20', - 'adjacent open end' => '10,20', - 'adjacent left' => '10,20', - 'adjacent right' => '10,20', - 'contained' => '10,10;20,20', - 'contained same start' => '20,20', - 'contained same end' => '10,10', + 'disjoint open start' => '[10...20]', + 'disjoint open end' => '[10...20]', + 'disjoint left' => '[10...20]', + 'disjoint right' => '[10...20]', + 'adjacent open start' => '[10...20]', + 'adjacent open end' => '[10...20]', + 'adjacent left' => '[10...20]', + 'adjacent right' => '[10...20]', + 'contained' => '[10...10] [20...20]', + 'contained same start' => '[20...20]', + 'contained same end' => '[10...10]', 'containing' => '', 'containing unbounded start' => '', 'containing unbounded end' => '', @@ -897,19 +898,19 @@ class IntegerRangeTest extends TestCase [ 'same' => '', 'unbounded' => '', - 'intersecting open start left' => '20,20', + 'intersecting open start left' => '[20...20]', 'intersecting open start right' => '', - 'intersecting bounded start left' => 'null,8;20,20', - 'intersecting bounded start right' => 'null,10', - 'intersecting bounded start center' => 'null,9', - 'intersecting open end same' => 'null,19', - 'intersecting open end left' => 'null,18', - 'intersecting bounded end same' => 'null,19', - 'intersecting bounded end left' => 'null,18', - 'adjacent open end' => 'null,20', - 'adjacent bounded end' => 'null,20', - 'disjoint open end' => 'null,20', - 'disjoint bounded end' => 'null,20', + 'intersecting bounded start left' => '[...8] [20...20]', + 'intersecting bounded start right' => '[...10]', + 'intersecting bounded start center' => '[...9]', + 'intersecting open end same' => '[...19]', + 'intersecting open end left' => '[...18]', + 'intersecting bounded end same' => '[...19]', + 'intersecting bounded end left' => '[...18]', + 'adjacent open end' => '[...20]', + 'adjacent bounded end' => '[...20]', + 'disjoint open end' => '[...20]', + 'disjoint bounded end' => '[...20]', ], $this->scenarioResults( new IntegerRange(null, 20), @@ -922,18 +923,18 @@ class IntegerRangeTest extends TestCase 'same' => '', 'unbounded' => '', 'intersecting open end left' => '', - 'intersecting open end right' => '10,10', - 'intersecting bounded end left' => '20,null', - 'intersecting bounded end right' => '10,10;22,null', - 'intersecting bounded end center' => '21,null', - 'intersecting open start same' => '11,null', - 'intersecting open start right' => '12,null', - 'intersecting bounded start same' => '11,null', - 'intersecting bounded start right' => '12,null', - 'adjacent open start' => '10,null', - 'adjacent bounded start' => '10,null', - 'disjoint open start' => '10,null', - 'disjoint bounded start' => '10,null', + 'intersecting open end right' => '[10...10]', + 'intersecting bounded end left' => '[20...]', + 'intersecting bounded end right' => '[10...10] [22...]', + 'intersecting bounded end center' => '[21...]', + 'intersecting open start same' => '[11...]', + 'intersecting open start right' => '[12...]', + 'intersecting bounded start same' => '[11...]', + 'intersecting bounded start right' => '[12...]', + 'adjacent open start' => '[10...]', + 'adjacent bounded start' => '[10...]', + 'disjoint open start' => '[10...]', + 'disjoint bounded start' => '[10...]', ], $this->scenarioResults( new IntegerRange(10, null), @@ -944,9 +945,9 @@ class IntegerRangeTest extends TestCase $this->assertEquals( [ 'same' => '', - 'open start' => '21,null', - 'open end' => 'null,9', - 'bounded' => 'null,9;21,null', + 'open start' => '[21...]', + 'open end' => '[...9]', + 'bounded' => '[...9] [21...]', ], $this->scenarioResults( new IntegerRange(null, null), @@ -961,31 +962,31 @@ class IntegerRangeTest extends TestCase $this->assertEquals( [ 'same' => '', - 'unbounded' => 'null,9;21,null', - 'intersecting open end left' => '9,9;21,null', - 'intersecting open end right' => '10,10;21,null', - 'intersecting open end center' => '21,null', - 'intersecting open start left' => 'null,9;20,20', - 'intersecting open start right' => 'null,9;21,21', - 'intersecting open start center' => 'null,9', - 'disjoint open start' => 'null,8;10,20', - 'disjoint open end' => '10,20;22,null', - 'disjoint left' => '-2,8;10,20', - 'disjoint right' => '10,20;22,32', - 'adjacent open start' => 'null,20', - 'adjacent open end' => '10,null', - 'adjacent left' => '-1,20', - 'adjacent right' => '10,31', - 'contained' => '10,10;20,20', - 'contained same start' => '20,20', - 'contained same end' => '10,10', - 'containing' => '9,9;21,21', - 'containing unbounded start' => 'null,9;21,21', - 'containing unbounded end' => '9,9;21,null', - 'containing same start' => '21,21', - 'containing same end' => '9,9', - 'containing same start unbounded end' => '21,null', - 'containing same end unbounded start' => 'null,9', + 'unbounded' => '[...9] [21...]', + 'intersecting open end left' => '[9...9] [21...]', + 'intersecting open end right' => '[10...10] [21...]', + 'intersecting open end center' => '[21...]', + 'intersecting open start left' => '[...9] [20...20]', + 'intersecting open start right' => '[...9] [21...21]', + 'intersecting open start center' => '[...9]', + 'disjoint open start' => '[...8] [10...20]', + 'disjoint open end' => '[10...20] [22...]', + 'disjoint left' => '[-2...8] [10...20]', + 'disjoint right' => '[10...20] [22...32]', + 'adjacent open start' => '[...20]', + 'adjacent open end' => '[10...]', + 'adjacent left' => '[-1...20]', + 'adjacent right' => '[10...31]', + 'contained' => '[10...10] [20...20]', + 'contained same start' => '[20...20]', + 'contained same end' => '[10...10]', + 'containing' => '[9...9] [21...21]', + 'containing unbounded start' => '[...9] [21...21]', + 'containing unbounded end' => '[9...9] [21...]', + 'containing same start' => '[21...21]', + 'containing same end' => '[9...9]', + 'containing same start unbounded end' => '[21...]', + 'containing same end unbounded start' => '[...9]', ], $this->scenarioResults( new IntegerRange(10, 20), @@ -996,20 +997,20 @@ class IntegerRangeTest extends TestCase $this->assertEquals( [ 'same' => '', - 'unbounded' => '21,null', - 'intersecting open start left' => '20,20', - 'intersecting open start right' => '21,21', - 'intersecting bounded start left' => 'null,8;20,20', - 'intersecting bounded start right' => 'null,10;21,21', - 'intersecting bounded start center' => 'null,9', - 'intersecting open end same' => 'null,19;21,null', - 'intersecting open end left' => 'null,18;21,null', - 'intersecting bounded end same' => 'null,19;21,30', - 'intersecting bounded end left' => 'null,18;21,29', - 'adjacent open end' => 'null,null', - 'adjacent bounded end' => 'null,30', - 'disjoint open end' => 'null,20;22,null', - 'disjoint bounded end' => 'null,20;22,32', + 'unbounded' => '[21...]', + 'intersecting open start left' => '[20...20]', + 'intersecting open start right' => '[21...21]', + 'intersecting bounded start left' => '[...8] [20...20]', + 'intersecting bounded start right' => '[...10] [21...21]', + 'intersecting bounded start center' => '[...9]', + 'intersecting open end same' => '[...19] [21...]', + 'intersecting open end left' => '[...18] [21...]', + 'intersecting bounded end same' => '[...19] [21...30]', + 'intersecting bounded end left' => '[...18] [21...29]', + 'adjacent open end' => '[...]', + 'adjacent bounded end' => '[...30]', + 'disjoint open end' => '[...20] [22...]', + 'disjoint bounded end' => '[...20] [22...32]', ], $this->scenarioResults( new IntegerRange(null, 20), @@ -1020,20 +1021,20 @@ class IntegerRangeTest extends TestCase $this->assertEquals( [ 'same' => '', - 'unbounded' => 'null,9', - 'intersecting open end left' => '9,9', - 'intersecting open end right' => '10,10', - 'intersecting bounded end left' => '9,9;20,null', - 'intersecting bounded end right' => '10,10;22,null', - 'intersecting bounded end center' => '21,null', - 'intersecting open start same' => 'null,9;11,null', - 'intersecting open start right' => 'null,9;12,null', - 'intersecting bounded start same' => '0,9;11,null', - 'intersecting bounded start right' => '1,9;12,null', - 'adjacent open start' => 'null,null', - 'adjacent bounded start' => '0,null', - 'disjoint open start' => 'null,8;10,null', - 'disjoint bounded start' => '-2,8;10,null', + 'unbounded' => '[...9]', + 'intersecting open end left' => '[9...9]', + 'intersecting open end right' => '[10...10]', + 'intersecting bounded end left' => '[9...9] [20...]', + 'intersecting bounded end right' => '[10...10] [22...]', + 'intersecting bounded end center' => '[21...]', + 'intersecting open start same' => '[...9] [11...]', + 'intersecting open start right' => '[...9] [12...]', + 'intersecting bounded start same' => '[0...9] [11...]', + 'intersecting bounded start right' => '[1...9] [12...]', + 'adjacent open start' => '[...]', + 'adjacent bounded start' => '[0...]', + 'disjoint open start' => '[...8] [10...]', + 'disjoint bounded start' => '[-2...8] [10...]', ], $this->scenarioResults( new IntegerRange(10, null), @@ -1044,9 +1045,9 @@ class IntegerRangeTest extends TestCase $this->assertEquals( [ 'same' => '', - 'open start' => '21,null', - 'open end' => 'null,9', - 'bounded' => 'null,9;21,null', + 'open start' => '[21...]', + 'open end' => '[...9]', + 'bounded' => '[...9] [21...]', ], $this->scenarioResults( new IntegerRange(null, null), @@ -1060,32 +1061,32 @@ class IntegerRangeTest extends TestCase // fully bounded $this->assertEquals( [ - 'same' => '10,20', - 'unbounded' => 'null,9;10,20;21,null', - 'intersecting open end left' => '9,9;10,20;21,null', - 'intersecting open end right' => '10,10;11,20;21,null', - 'intersecting open end center' => '10,20;21,null', - 'intersecting open start left' => 'null,9;10,19;20,20', - 'intersecting open start right' => 'null,9;10,20;21,21', - 'intersecting open start center' => 'null,9;10,20', - 'disjoint open start' => 'null,8;10,20', - 'disjoint open end' => '10,20;22,null', - 'disjoint left' => '-2,8;10,20', - 'disjoint right' => '10,20;22,32', - 'adjacent open start' => 'null,9;10,20', - 'adjacent open end' => '10,20;21,null', - 'adjacent left' => '-1,9;10,20', - 'adjacent right' => '10,20;21,31', - 'contained' => '10,10;11,19;20,20', - 'contained same start' => '10,19;20,20', - 'contained same end' => '10,10;11,20', - 'containing' => '9,9;10,20;21,21', - 'containing unbounded start' => 'null,9;10,20;21,21', - 'containing unbounded end' => '9,9;10,20;21,null', - 'containing same start' => '10,20;21,21', - 'containing same end' => '9,9;10,20', - 'containing same start unbounded end' => '10,20;21,null', - 'containing same end unbounded start' => 'null,9;10,20', + 'same' => '[10...20]', + 'unbounded' => '[...9] [10...20] [21...]', + 'intersecting open end left' => '[9...9] [10...20] [21...]', + 'intersecting open end right' => '[10...10] [11...20] [21...]', + 'intersecting open end center' => '[10...20] [21...]', + 'intersecting open start left' => '[...9] [10...19] [20...20]', + 'intersecting open start right' => '[...9] [10...20] [21...21]', + 'intersecting open start center' => '[...9] [10...20]', + 'disjoint open start' => '[...8] [10...20]', + 'disjoint open end' => '[10...20] [22...]', + 'disjoint left' => '[-2...8] [10...20]', + 'disjoint right' => '[10...20] [22...32]', + 'adjacent open start' => '[...9] [10...20]', + 'adjacent open end' => '[10...20] [21...]', + 'adjacent left' => '[-1...9] [10...20]', + 'adjacent right' => '[10...20] [21...31]', + 'contained' => '[10...10] [11...19] [20...20]', + 'contained same start' => '[10...19] [20...20]', + 'contained same end' => '[10...10] [11...20]', + 'containing' => '[9...9] [10...20] [21...21]', + 'containing unbounded start' => '[...9] [10...20] [21...21]', + 'containing unbounded end' => '[9...9] [10...20] [21...]', + 'containing same start' => '[10...20] [21...21]', + 'containing same end' => '[9...9] [10...20]', + 'containing same start unbounded end' => '[10...20] [21...]', + 'containing same end unbounded start' => '[...9] [10...20]', ], $this->scenarioResults( new IntegerRange(10, 20), @@ -1095,21 +1096,21 @@ class IntegerRangeTest extends TestCase // open start $this->assertEquals( [ - 'same' => 'null,20', - 'unbounded' => 'null,20;21,null', - 'intersecting open start left' => 'null,19;20,20', - 'intersecting open start right' => 'null,20;21,21', - 'intersecting bounded start left' => 'null,8;9,19;20,20', - 'intersecting bounded start right' => 'null,10;11,20;21,21', - 'intersecting bounded start center' => 'null,9;10,20', - 'intersecting open end same' => 'null,19;20,20;21,null', - 'intersecting open end left' => 'null,18;19,20;21,null', - 'intersecting bounded end same' => 'null,19;20,20;21,30', - 'intersecting bounded end left' => 'null,18;19,20;21,29', - 'adjacent open end' => 'null,20;21,null', - 'adjacent bounded end' => 'null,20;21,30', - 'disjoint open end' => 'null,20;22,null', - 'disjoint bounded end' => 'null,20;22,32', + 'same' => '[...20]', + 'unbounded' => '[...20] [21...]', + 'intersecting open start left' => '[...19] [20...20]', + 'intersecting open start right' => '[...20] [21...21]', + 'intersecting bounded start left' => '[...8] [9...19] [20...20]', + 'intersecting bounded start right' => '[...10] [11...20] [21...21]', + 'intersecting bounded start center' => '[...9] [10...20]', + 'intersecting open end same' => '[...19] [20...20] [21...]', + 'intersecting open end left' => '[...18] [19...20] [21...]', + 'intersecting bounded end same' => '[...19] [20...20] [21...30]', + 'intersecting bounded end left' => '[...18] [19...20] [21...29]', + 'adjacent open end' => '[...20] [21...]', + 'adjacent bounded end' => '[...20] [21...30]', + 'disjoint open end' => '[...20] [22...]', + 'disjoint bounded end' => '[...20] [22...32]', ], $this->scenarioResults( new IntegerRange(null, 20), @@ -1119,21 +1120,21 @@ class IntegerRangeTest extends TestCase // open end $this->assertEquals( [ - 'same' => '10,null', - 'unbounded' => 'null,9;10,null', - 'intersecting open end left' => '9,9;10,null', - 'intersecting open end right' => '10,10;11,null', - 'intersecting bounded end left' => '9,9;10,19;20,null', - 'intersecting bounded end right' => '10,10;11,21;22,null', - 'intersecting bounded end center' => '10,20;21,null', - 'intersecting open start same' => 'null,9;10,10;11,null', - 'intersecting open start right' => 'null,9;10,11;12,null', - 'intersecting bounded start same' => '0,9;10,10;11,null', - 'intersecting bounded start right' => '1,9;10,11;12,null', - 'adjacent open start' => 'null,9;10,null', - 'adjacent bounded start' => '0,9;10,null', - 'disjoint open start' => 'null,8;10,null', - 'disjoint bounded start' => '-2,8;10,null', + 'same' => '[10...]', + 'unbounded' => '[...9] [10...]', + 'intersecting open end left' => '[9...9] [10...]', + 'intersecting open end right' => '[10...10] [11...]', + 'intersecting bounded end left' => '[9...9] [10...19] [20...]', + 'intersecting bounded end right' => '[10...10] [11...21] [22...]', + 'intersecting bounded end center' => '[10...20] [21...]', + 'intersecting open start same' => '[...9] [10...10] [11...]', + 'intersecting open start right' => '[...9] [10...11] [12...]', + 'intersecting bounded start same' => '[0...9] [10...10] [11...]', + 'intersecting bounded start right' => '[1...9] [10...11] [12...]', + 'adjacent open start' => '[...9] [10...]', + 'adjacent bounded start' => '[0...9] [10...]', + 'disjoint open start' => '[...8] [10...]', + 'disjoint bounded start' => '[-2...8] [10...]', ], $this->scenarioResults( new IntegerRange(10, null), @@ -1143,10 +1144,10 @@ class IntegerRangeTest extends TestCase // fully unbounded $this->assertEquals( [ - 'same' => 'null,null', - 'open start' => 'null,20;21,null', - 'open end' => 'null,9;10,null', - 'bounded' => 'null,9;10,20;21,null', + 'same' => '[...]', + 'open start' => '[...20] [21...]', + 'open end' => '[...9] [10...]', + 'bounded' => '[...9] [10...20] [21...]', ], $this->scenarioResults( new IntegerRange(null, null), @@ -1242,15 +1243,10 @@ class IntegerRangeTest extends TestCase function ($s) use ($range, $method) { $result = $range->$method($s); if ($result instanceof IntegerRange) { - $result = ($result->start() ?? 'null') . ',' . ($result->end() ?? 'null'); + $result = (string)$result; } - if (is_array($result)) { - $result = implode(';', array_map( - function ($r) { - return ($r->start() ?? 'null') . ',' . ($r->end() ?? 'null'); - }, - $result - )); + if ($result instanceof RangeCollection) { + $result = implode(' ', $result->toArray()); } return $result; },