From d983b7e6bf43c6e8253b84eee289684ea4547717 Mon Sep 17 00:00:00 2001 From: azjezz Date: Sat, 26 Sep 2020 06:17:33 +0100 Subject: [PATCH] [DataStructure] Introduce the DataStructure component --- src/Psl/DataStructure/PriorityQueue.php | 137 ++++++++++++++++++ .../DataStructure/PriorityQueueInterface.php | 51 +++++++ src/Psl/DataStructure/Queue.php | 83 +++++++++++ src/Psl/DataStructure/QueueInterface.php | 51 +++++++ src/Psl/Iter/Iterator.php | 6 +- tests/Psl/DataStructure/PriorityQueueTest.php | 98 +++++++++++++ tests/Psl/DataStructure/QueueTest.php | 83 +++++++++++ 7 files changed, 506 insertions(+), 3 deletions(-) create mode 100644 src/Psl/DataStructure/PriorityQueue.php create mode 100644 src/Psl/DataStructure/PriorityQueueInterface.php create mode 100644 src/Psl/DataStructure/Queue.php create mode 100644 src/Psl/DataStructure/QueueInterface.php create mode 100644 tests/Psl/DataStructure/PriorityQueueTest.php create mode 100644 tests/Psl/DataStructure/QueueTest.php diff --git a/src/Psl/DataStructure/PriorityQueue.php b/src/Psl/DataStructure/PriorityQueue.php new file mode 100644 index 00000000..6c09b2f0 --- /dev/null +++ b/src/Psl/DataStructure/PriorityQueue.php @@ -0,0 +1,137 @@ + + */ +final class PriorityQueue implements PriorityQueueInterface +{ + /** + * @psalm-var array> + */ + private array $queue = []; + + /** + * Adds a node to the queue. + * + * @psalm-param T $node + */ + public function enqueue(int $priority, $node): void + { + $nodes = $this->queue[$priority] ?? []; + $nodes[] = $node; + + $this->queue[$priority] = $nodes; + $this->queue = Arr\filter( + $this->queue, + static fn(array $list): bool => Arr\count($list) !== 0 + ); + } + + /** + * Retrieves, but does not remove, the node at the head of this queue, + * or returns null if this queue is empty. + * + * @psalm-return null|T + */ + public function peek() + { + if (0 === $this->count()) { + return null; + } + + /** @psalm-suppress MissingThrowsDocblock - we are sure that the queue is not empty. */ + return $this->fetch(false); + } + + /** + * Retrieves and removes the node at the head of this queue, + * or returns null if this queue is empty. + * + * @psalm-return null|T + */ + public function pull() + { + if (0 === $this->count()) { + return null; + } + + /** @psalm-suppress MissingThrowsDocblock - we are sure that the queue is not empty. */ + return $this->fetch(true); + } + + /** + * Dequeues a node from the queue. + * + * @psalm-return T + * + * @throws Psl\Exception\InvariantViolationException If the Queue is invalid. + */ + public function dequeue() + { + return $this->fetch(true); + } + + /** + * @psalm-return T + * + * @throws Psl\Exception\InvariantViolationException If the Queue is invalid. + */ + private function fetch(bool $remove) + { + Psl\invariant(0 !== $this->count(), 'Cannot dequeue a node from an empty Queue.'); + + // Retrieve the list of priorities. + $priorities = Arr\keys($this->queue); + /** + * Retrieve the highest priority. + * + * @var int $priority + */ + $priority = Math\max($priorities); + // Retrieve the list of nodes with the priority `$priority`. + $nodes = Arr\at($this->queue, $priority); + /** + * Retrieve the first node of the list. + * + * @psalm-suppress MissingThrowsDocblock - we are sure that the list is not empty. + */ + $node = Arr\firstx($nodes); + + // Remove the node if we are supposed to. + if ($remove) { + // If the list contained only this node, + // remove the list of nodes with priority `$priority`. + if (1 === Arr\count($nodes)) { + unset($this->queue[$priority]); + } else { + // otherwise, drop the first node. + $this->queue[$priority] = Arr\values(Arr\drop($nodes, 1)); + } + } + + return $node; + } + + /** + * Count the nodes in the queue. + */ + public function count(): int + { + $count = 0; + foreach ($this->queue as $priority => $list) { + $count += Arr\count($list); + } + + return $count; + } +} diff --git a/src/Psl/DataStructure/PriorityQueueInterface.php b/src/Psl/DataStructure/PriorityQueueInterface.php new file mode 100644 index 00000000..e6045d97 --- /dev/null +++ b/src/Psl/DataStructure/PriorityQueueInterface.php @@ -0,0 +1,51 @@ + + */ +final class Queue implements QueueInterface +{ + /** + * @psalm-var list + */ + private array $queue = []; + + /** + * Adds a node to the queue. + * + * @psalm-param T $node + */ + public function enqueue($node): void + { + $this->queue[] = $node; + } + + /** + * Retrieves, but does not remove, the node at the head of this queue, + * or returns null if this queue is empty. + * + * @psalm-return null|T + */ + public function peek() + { + return Arr\first($this->queue); + } + + /** + * Retrieves and removes the node at the head of this queue, + * or returns null if this queue is empty. + * + * @psalm-return null|T + */ + public function pull() + { + if (0 === $this->count()) { + return null; + } + + /** @psalm-suppress MissingThrowsDocblock - we are sure that the queue is not empty. */ + return $this->dequeue(); + } + + /** + * Dequeues a node from the queue. + * + * @psalm-return T + * + * @throws Psl\Exception\InvariantViolationException If the Queue is invalid. + */ + public function dequeue() + { + Psl\invariant(0 !== $this->count(), 'Cannot dequeue a node from an empty Queue.'); + + $node = Arr\firstx($this->queue); + $this->queue = Arr\values(Arr\drop($this->queue, 1)); + + return $node; + } + + /** + * Count the nodes in the queue. + */ + public function count(): int + { + return Arr\count($this->queue); + } +} diff --git a/src/Psl/DataStructure/QueueInterface.php b/src/Psl/DataStructure/QueueInterface.php new file mode 100644 index 00000000..2395c578 --- /dev/null +++ b/src/Psl/DataStructure/QueueInterface.php @@ -0,0 +1,51 @@ +) $factory */ - $factory = fn (): Generator => yield from $iterable; + $factory = static fn (): Generator => yield from $iterable; return new self($factory()); } @@ -77,7 +77,7 @@ public static function create(iterable $iterable): Iterator */ public function current() { - Psl\invariant($this->valid(), 'Invalid iterator'); + Psl\invariant($this->valid(), 'The Iterator is invalid.'); if (!Arr\contains_key($this->entries, $this->position)) { $this->progress(); } @@ -117,7 +117,7 @@ public function next(): void */ public function key() { - Psl\invariant($this->valid(), 'Invalid iterator'); + Psl\invariant($this->valid(), 'The Iterator is invalid.'); if (!Arr\contains_key($this->entries, $this->position)) { $this->progress(); } diff --git a/tests/Psl/DataStructure/PriorityQueueTest.php b/tests/Psl/DataStructure/PriorityQueueTest.php new file mode 100644 index 00000000..ad3a9e6e --- /dev/null +++ b/tests/Psl/DataStructure/PriorityQueueTest.php @@ -0,0 +1,98 @@ +enqueue(1, 'hi'); + $queue->enqueue(2, 'hey'); + $queue->enqueue(3, 'hello'); + + self::assertCount(3, $queue); + self::assertSame('hello', $queue->dequeue()); + self::assertCount(2, $queue); + self::assertSame('hey', $queue->dequeue()); + self::assertCount(1, $queue); + self::assertSame('hi', $queue->dequeue()); + } + + public function testMultipleNodesWithSamePriority(): void + { + $queue = new DataStructure\PriorityQueue(); + $queue->enqueue(1, 'hi'); + $queue->enqueue(1, 'hey'); + $queue->enqueue(1, 'hello'); + + self::assertCount(3, $queue); + self::assertSame('hi', $queue->dequeue()); + self::assertCount(2, $queue); + self::assertSame('hey', $queue->dequeue()); + self::assertCount(1, $queue); + self::assertSame('hello', $queue->dequeue()); + } + + public function testPeekDoesNotRemoveTheNode(): void + { + $queue = new DataStructure\PriorityQueue(); + $queue->enqueue(1, 'hi'); + $queue->enqueue(2, 'hey'); + $queue->enqueue(3, 'hello'); + + self::assertCount(3, $queue); + self::assertSame('hello', $queue->peek()); + self::assertCount(3, $queue); + self::assertSame('hello', $queue->peek()); + } + + public function testPeekReturnsNullWhenTheQueueIsEmpty(): void + { + $queue = new DataStructure\PriorityQueue(); + + self::assertCount(0, $queue); + self::assertNull($queue->peek()); + } + + public function testPullDoesRemoveTheNode(): void + { + $queue = new DataStructure\PriorityQueue(); + $queue->enqueue(1, 'hi'); + $queue->enqueue(2, 'hey'); + $queue->enqueue(3, 'hello'); + + self::assertCount(3, $queue); + self::assertSame('hello', $queue->pull()); + self::assertCount(2, $queue); + self::assertSame('hey', $queue->pull()); + self::assertCount(1, $queue); + self::assertSame('hi', $queue->pull()); + self::assertCount(0, $queue); + self::assertNull($queue->pull()); + } + + public function testPullReturnsNullWhenTheQueueIsEmpty(): void + { + $queue = new DataStructure\PriorityQueue(); + + self::assertCount(0, $queue); + self::assertNull($queue->pull()); + } + + public function testDequeueThrowsWhenTheQueueIsEmpty(): void + { + $queue = new DataStructure\PriorityQueue(); + + $this->expectException(Psl\Exception\InvariantViolationException::class); + $this->expectExceptionMessage('Cannot dequeue a node from an empty Queue.'); + + $queue->dequeue(); + } +} diff --git a/tests/Psl/DataStructure/QueueTest.php b/tests/Psl/DataStructure/QueueTest.php new file mode 100644 index 00000000..2c139895 --- /dev/null +++ b/tests/Psl/DataStructure/QueueTest.php @@ -0,0 +1,83 @@ +enqueue('hello'); + $queue->enqueue('hey'); + $queue->enqueue('hi'); + + self::assertCount(3, $queue); + self::assertSame('hello', $queue->dequeue()); + self::assertCount(2, $queue); + self::assertSame('hey', $queue->dequeue()); + self::assertCount(1, $queue); + self::assertSame('hi', $queue->dequeue()); + } + + public function testPeekDoesNotRemoveTheNode(): void + { + $queue = new DataStructure\Queue(); + $queue->enqueue('hello'); + $queue->enqueue('hey'); + $queue->enqueue('hi'); + + self::assertCount(3, $queue); + self::assertSame('hello', $queue->peek()); + self::assertCount(3, $queue); + self::assertSame('hello', $queue->peek()); + } + + public function testPeekReturnsNullWhenTheQueueIsEmpty(): void + { + $queue = new DataStructure\Queue(); + + self::assertCount(0, $queue); + self::assertNull($queue->peek()); + } + + public function testPullDoesRemoveTheNode(): void + { + $queue = new DataStructure\Queue(); + $queue->enqueue('hello'); + $queue->enqueue('hey'); + $queue->enqueue('hi'); + + self::assertCount(3, $queue); + self::assertSame('hello', $queue->pull()); + self::assertCount(2, $queue); + self::assertSame('hey', $queue->pull()); + self::assertCount(1, $queue); + self::assertSame('hi', $queue->pull()); + self::assertCount(0, $queue); + self::assertNull($queue->pull()); + } + + public function testPullReturnsNullWhenTheQueueIsEmpty(): void + { + $queue = new DataStructure\Queue(); + + self::assertCount(0, $queue); + self::assertNull($queue->pull()); + } + + public function testDequeueThrowsWhenTheQueueIsEmpty(): void + { + $queue = new DataStructure\Queue(); + + $this->expectException(Psl\Exception\InvariantViolationException::class); + $this->expectExceptionMessage('Cannot dequeue a node from an empty Queue.'); + + $queue->dequeue(); + } +}