diff --git a/src/Psl/DataStructure/PriorityQueue.php b/src/Psl/DataStructure/PriorityQueue.php new file mode 100644 index 000000000..3228443d1 --- /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($node, int $priority = 0): 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 000000000..61decf2d6 --- /dev/null +++ b/src/Psl/DataStructure/PriorityQueueInterface.php @@ -0,0 +1,23 @@ + + */ +interface PriorityQueueInterface extends QueueInterface +{ + /** + * Adds a node to the queue. + * + * @psalm-param T $node + */ + public function enqueue($node, int $priority = 0): void; +} diff --git a/src/Psl/DataStructure/Queue.php b/src/Psl/DataStructure/Queue.php new file mode 100644 index 000000000..17fd6d896 --- /dev/null +++ b/src/Psl/DataStructure/Queue.php @@ -0,0 +1,85 @@ + + */ +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 000000000..f7f85c1bb --- /dev/null +++ b/src/Psl/DataStructure/QueueInterface.php @@ -0,0 +1,55 @@ + + */ +final class Stack implements StackInterface +{ + /** + * @psalm-var list $items + */ + private array $items = []; + + /** + * Adds an item to the stack. + * + * @psalm-param T $item + */ + public function push($item): void + { + $this->items[] = $item; + } + + /** + * Retrieves, but does remove, the most recently added item that was not yet removed, + * or returns null if this queue is empty. + * + * @psalm-return null|T + */ + public function peek() + { + return Arr\last($this->items); + } + + /** + * Retrieves and removes the most recently added item that was not yet removed, + * or returns null if this queue is empty. + * + * @psalm-return null|T + */ + public function pull() + { + if (0 === $this->count()) { + return null; + } + + /** @psalm-suppress MissingThrowsDocblock - the stack is not empty. */ + return $this->pop(); + } + + /** + * Retrieve and removes the most recently added item that was not yet removed. + * + * @psalm-return T + * + * @throws Psl\Exception\InvariantViolationException If the stack is empty. + */ + public function pop() + { + Psl\invariant(0 !== $this->count(), 'Cannot pop an item from an empty Stack.'); + + $tail = Arr\lastx($this->items); + $this->items = Arr\values(Arr\take($this->items, $this->count() - 1)); + + return $tail; + } + + /** + * Count the items in the stack. + */ + public function count(): int + { + return Arr\count($this->items); + } +} diff --git a/src/Psl/DataStructure/StackInterface.php b/src/Psl/DataStructure/StackInterface.php new file mode 100644 index 000000000..191fe3350 --- /dev/null +++ b/src/Psl/DataStructure/StackInterface.php @@ -0,0 +1,55 @@ +) $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 000000000..997215525 --- /dev/null +++ b/tests/Psl/DataStructure/PriorityQueueTest.php @@ -0,0 +1,98 @@ +enqueue('hi', 1); + $queue->enqueue('hey', 2); + $queue->enqueue('hello', 3); + + 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('hi', 1); + $queue->enqueue('hey', 1); + $queue->enqueue('hello', 1); + + 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('hi', 1); + $queue->enqueue('hey', 2); + $queue->enqueue('hello', 3); + + 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('hi', 1); + $queue->enqueue('hey', 2); + $queue->enqueue('hello', 3); + + 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 000000000..2c1398952 --- /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(); + } +} diff --git a/tests/Psl/DataStructure/StackTest.php b/tests/Psl/DataStructure/StackTest.php new file mode 100644 index 000000000..da46d8547 --- /dev/null +++ b/tests/Psl/DataStructure/StackTest.php @@ -0,0 +1,73 @@ +push('hello'); + $stack->push('hey'); + $stack->push('hi'); + + self::assertCount(3, $stack); + + self::assertSame('hi', $stack->peek()); + + self::assertSame('hi', $stack->pop()); + self::assertSame('hey', $stack->pop()); + self::assertSame('hello', $stack->pop()); + + self::assertNull($stack->pull()); + } + + public function testPeek(): void + { + $stack = new Stack(); + + self::assertNull($stack->peek()); + + $stack->push('hello'); + + self::assertNotNull($stack->peek()); + self::assertSame('hello', $stack->peek()); + } + + public function testPopThrowsForEmptyStack(): void + { + $stack = new Stack(); + $stack->push('hello'); + + self::assertSame('hello', $stack->pop()); + + $this->expectException(Psl\Exception\InvariantViolationException::class); + $this->expectExceptionMessage('Cannot pop an item from an empty Stack.'); + + $stack->pop(); + } + + public function testPullReturnsNullForEmptyStack(): void + { + $stack = new Stack(); + $stack->push('hello'); + + self::assertSame('hello', $stack->pull()); + self::assertNull($stack->pull()); + } + + public function testCount(): void + { + $stack = new Stack(); + self::assertSame(0, $stack->count()); + + $stack->push('hello'); + self::assertSame(1, $stack->count()); + } +}