[DataStructure] Introduce the DataStructure component (#53)

This commit is contained in:
Saif Eddin G 2020-10-07 10:35:06 +02:00 committed by GitHub
parent f60c10629d
commit 1bebacb3ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 701 additions and 5 deletions

View File

@ -24,9 +24,9 @@ namespace Psl\Arr;
*
* @psalm-param array<Tk, Tv> $array
* @psalm-param Tk $index
* @psalm-param Tv|null $default
* @psalm-param Tv $default
*
* @psalm-return Tv|null
* @psalm-return Tv
*
* @psalm-pure
*/

View File

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Psl\DataStructure;
use Psl;
use Psl\Arr;
use Psl\Math;
/**
* @psalm-template T
*
* @implements PriorityQueueInterface<T>
*/
final class PriorityQueue implements PriorityQueueInterface
{
/**
* @psalm-var array<int, list<T>>
*/
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;
}
/**
* 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;
}
// Retrieve the highest priority.
$priority = Math\max(Arr\keys($this->queue)) ?? 0;
// Retrieve the list of nodes with the priority `$priority`.
$nodes = Arr\idx($this->queue, $priority, []);
// Retrieve the first node of the list.
return Arr\first($nodes);
}
/**
* 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 - 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.');
/**
* Peeking into a non-empty queue always results in a value.
*
* @psalm-var T $node
*/
$node = $this->peek();
$this->drop();
return $node;
}
private function drop(): void
{
/**
* Retrieve the highest priority.
*
* @var int $priority
*/
$priority = Math\max(Arr\keys($this->queue));
/**
* Retrieve the list of nodes with the priority `$priority`.
*
* @psalm-suppress MissingThrowsDocblock
*/
$nodes = Arr\at($this->queue, $priority);
// 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.
*
* @psalm-suppress MissingThrowsDocblock
*/
$this->queue[$priority] = Arr\values(Arr\drop($nodes, 1));
}
}
/**
* Count the nodes in the queue.
*/
public function count(): int
{
$count = 0;
foreach ($this->queue as $priority => $list) {
$count += Arr\count($list);
}
return $count;
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Psl\DataStructure;
use Psl;
use Countable;
/**
* @template T
*
* @extends QueueInterface<T>
*/
interface PriorityQueueInterface extends QueueInterface
{
/**
* Adds a node to the queue.
*
* @psalm-param T $node
*/
public function enqueue($node, int $priority = 0): void;
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Psl\DataStructure;
use Psl;
use Psl\Arr;
/**
* A basic implementation of a queue data structure ( FIFO ).
*
* @psalm-template T
*
* @implements QueueInterface<T>
*/
final class Queue implements QueueInterface
{
/**
* @psalm-var list<T>
*/
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);
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Psl\DataStructure;
use Psl;
use Countable;
/**
* An interface representing a queue data structure ( FIFO ).
*
* @template T
*
* @see https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
*/
interface QueueInterface extends Countable
{
/**
* Adds a node to the queue.
*
* @psalm-param T $node
*/
public function enqueue($node): void;
/**
* 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();
/**
* 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();
/**
* Retrieves and removes the node at the head of this queue.
*
* @psalm-return T
*
* @throws Psl\Exception\InvariantViolationException If the Queue is invalid.
*/
public function dequeue();
/**
* Count the nodes in the queue.
*/
public function count(): int;
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Psl\DataStructure;
use Psl;
use Psl\Arr;
/**
* An basic implementation of a stack data structure ( LIFO ).
*
* @template T
*
* @implements StackInterface<T>
*/
final class Stack implements StackInterface
{
/**
* @psalm-var list<T> $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);
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Psl\DataStructure;
use Psl;
use Countable;
/**
* An interface representing a stack data structure ( LIFO ).
*
* @template T
*
* @see https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
*/
interface StackInterface extends Countable
{
/**
* Adds an item to the stack.
*
* @psalm-param T $item
*/
public function push($item): void;
/**
* 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();
/**
* 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();
/**
* 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();
/**
* Count the items in the stack.
*/
public function count(): int;
}

View File

@ -63,7 +63,7 @@ final class Iterator implements SeekableIterator, Countable
/**
* @psalm-var (callable(): Generator<Tsk, Tsv, mixed, void>) $factory
*/
$factory = fn (): Generator => yield from $iterable;
$factory = static fn (): Generator => yield from $iterable;
return new self($factory());
}
@ -77,7 +77,7 @@ final class Iterator implements SeekableIterator, Countable
*/
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 @@ final class Iterator implements SeekableIterator, Countable
*/
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();
}

View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Psl\DataStructure;
use PHPUnit\Framework\TestCase;
use Psl;
use Psl\DataStructure;
final class PriorityQueueTest extends TestCase
{
public function testEnqueueAndDequeue(): 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->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();
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Psl\DataStructure;
use Psl;
use Psl\DataStructure;
use PHPUnit\Framework\TestCase;
final class QueueTest extends TestCase
{
public function testEnqueueAndDequeue(): void
{
$queue = new DataStructure\Queue();
$queue->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();
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Psl\Tests\DataStructure;
use Psl;
use Psl\DataStructure\Stack;
use PHPUnit\Framework\TestCase;
final class StackTest extends TestCase
{
public function testPushAndPop(): void
{
$stack = new Stack();
$stack->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());
}
}