added boolean AND, updated tests
This commit is contained in:
parent
91a5545c73
commit
4dcbc43009
2 changed files with 342 additions and 23 deletions
|
@ -68,13 +68,31 @@ abstract class AbstractRange
|
||||||
* @param T|null $start
|
* @param T|null $start
|
||||||
* @param T|null $end
|
* @param T|null $end
|
||||||
*/
|
*/
|
||||||
public function __construct($start, $end)
|
final public function __construct($start, $end)
|
||||||
{
|
{
|
||||||
$this->setStart($start);
|
$this->setStart($start);
|
||||||
$this->setEnd($end);
|
$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
|
* @param static $other
|
||||||
*/
|
*/
|
||||||
public function equals(AbstractRange $other): bool
|
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
|
* @param static $other
|
||||||
*/
|
*/
|
||||||
public function intersects(AbstractRange $other): bool
|
public function intersects(AbstractRange $other): bool
|
||||||
|
@ -93,6 +112,7 @@ abstract class AbstractRange
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Check if this range completely contains another range.
|
||||||
* @param static $other
|
* @param static $other
|
||||||
*/
|
*/
|
||||||
public function contains(AbstractRange $other): bool
|
public function contains(AbstractRange $other): bool
|
||||||
|
|
|
@ -25,7 +25,6 @@
|
||||||
|
|
||||||
namespace Joby\Toolbox\Sorting;
|
namespace Joby\Toolbox\Sorting;
|
||||||
|
|
||||||
use Exception;
|
|
||||||
use Joby\Toolbox\Ranges\IntegerRange;
|
use Joby\Toolbox\Ranges\IntegerRange;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
@ -341,7 +340,7 @@ class IntegerRangeTest extends TestCase
|
||||||
'contains',
|
'contains',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// fully unbounded
|
// fully unbounded contains anything
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
[
|
[
|
||||||
'same' => true,
|
'same' => true,
|
||||||
|
@ -356,13 +355,109 @@ class IntegerRangeTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: testAbutsEndOf
|
public function testAbutsEndOf()
|
||||||
|
{
|
||||||
// TODO: see about making failures go to the right line/test
|
// 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()
|
public function testAbutsStartOf()
|
||||||
{
|
{
|
||||||
// fully bounded
|
// fully bounded only abuts the start of things that are adjacent right
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
[
|
[
|
||||||
'same' => false,
|
'same' => false,
|
||||||
|
@ -397,7 +492,7 @@ class IntegerRangeTest extends TestCase
|
||||||
'abutsStartOf',
|
'abutsStartOf',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// open start
|
// open start only abuts the start of things that are adjacent right
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
[
|
[
|
||||||
'same' => false,
|
'same' => false,
|
||||||
|
@ -421,7 +516,7 @@ class IntegerRangeTest extends TestCase
|
||||||
'abutsStartOf',
|
'abutsStartOf',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// open end
|
// open end can't abut the start of anything
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
[
|
[
|
||||||
'same' => false,
|
'same' => false,
|
||||||
|
@ -445,7 +540,7 @@ class IntegerRangeTest extends TestCase
|
||||||
'abutsStartOf',
|
'abutsStartOf',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// fully unbounded
|
// fully unbounded can't abut anything
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
[
|
[
|
||||||
'same' => false,
|
'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
|
protected function createScenarios(IntegerRange $range): array
|
||||||
{
|
{
|
||||||
if (is_null($range->start()) && is_null($range->end())) {
|
if (is_null($range->start()) && is_null($range->end())) {
|
||||||
// scenarios for fully open range
|
// scenarios for fully open range
|
||||||
return [
|
return [
|
||||||
'same' => new IntegerRange(null, null),
|
'same' => new IntegerRange(null, null),
|
||||||
'open start' => new IntegerRange(null, 10),
|
'open start' => new IntegerRange(null, 20),
|
||||||
'open end' => new IntegerRange(10, null),
|
'open end' => new IntegerRange(10, null),
|
||||||
'bounded' => new IntegerRange(10, 20),
|
'bounded' => new IntegerRange(10, 20),
|
||||||
];
|
];
|
||||||
|
@ -515,26 +810,26 @@ class IntegerRangeTest extends TestCase
|
||||||
'unbounded' => new IntegerRange(null, null),
|
'unbounded' => new IntegerRange(null, null),
|
||||||
'intersecting open end left' => new IntegerRange($range->start() - 2, null),
|
'intersecting open end left' => new IntegerRange($range->start() - 2, null),
|
||||||
'intersecting open end right' => 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 left' => new IntegerRange(null, $range->end() - 2),
|
||||||
'intersecting open start right' => 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 start' => new IntegerRange(null, $range->start() - 2),
|
||||||
'disjoint open end' => new IntegerRange($range->end() + 2, null),
|
'disjoint open end' => new IntegerRange($range->end() + 2, null),
|
||||||
'disjoint left' => new IntegerRange($range->start() - 10, $range->start() - 2),
|
'disjoint left' => new IntegerRange($range->start() - 12, $range->start() - 2),
|
||||||
'disjoint right' => new IntegerRange($range->end() + 2, $range->end() + 10),
|
'disjoint right' => new IntegerRange($range->end() + 2, $range->end() + 12),
|
||||||
'adjacent open start' => new IntegerRange(null, $range->start() - 1),
|
'adjacent open start' => new IntegerRange(null, $range->start() - 1),
|
||||||
'adjacent open end' => new IntegerRange($range->end() + 1, null),
|
'adjacent open end' => new IntegerRange($range->end() + 1, null),
|
||||||
'adjacent left' => new IntegerRange($range->start() - 1, $range->start() - 1),
|
'adjacent left' => new IntegerRange($range->start() - 11, $range->start() - 1),
|
||||||
'adjacent right' => new IntegerRange($range->end() + 1, $range->end() + 1),
|
'adjacent right' => new IntegerRange($range->end() + 1, $range->end() + 11),
|
||||||
'contained' => new IntegerRange($range->start() + 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 start' => new IntegerRange($range->start(), $range->end() - 1),
|
||||||
'contained same end' => new IntegerRange($range->start() + 1, $range->end()),
|
'contained same end' => new IntegerRange($range->start() + 1, $range->end()),
|
||||||
'containing' => new IntegerRange($range->start() - 2, $range->end() + 2),
|
'containing' => new IntegerRange($range->start() - 1, $range->end() + 1),
|
||||||
'containing unbounded start' => new IntegerRange(null, $range->end() + 2),
|
'containing unbounded start' => new IntegerRange(null, $range->end() + 1),
|
||||||
'containing unbounded end' => new IntegerRange($range->start() - 2, null),
|
'containing unbounded end' => new IntegerRange($range->start() - 1, null),
|
||||||
'containing same start' => new IntegerRange($range->start(), $range->end() + 2),
|
'containing same start' => new IntegerRange($range->start(), $range->end() + 1),
|
||||||
'containing same end' => new IntegerRange($range->start() - 2, $range->end()),
|
'containing same end' => new IntegerRange($range->start() - 1, $range->end()),
|
||||||
'containing same start unbounded end' => new IntegerRange($range->start(), null),
|
'containing same start unbounded end' => new IntegerRange($range->start(), null),
|
||||||
'containing same end unbounded start' => new IntegerRange(null, $range->end()),
|
'containing same end unbounded start' => new IntegerRange(null, $range->end()),
|
||||||
];
|
];
|
||||||
|
@ -545,7 +840,11 @@ class IntegerRangeTest extends TestCase
|
||||||
{
|
{
|
||||||
return array_map(
|
return array_map(
|
||||||
function ($s) use ($range, $method) {
|
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)
|
$this->createScenarios($range)
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue