Reimplement data structures based on SPL

This commit is contained in:
Sebastian Meyer 2023-11-12 15:34:08 +01:00
parent dc4209e40f
commit a84046755e
4 changed files with 146 additions and 256 deletions

View File

@ -1,111 +0,0 @@
<?php
/**
* Useful PHP Basics
* Copyright (C) 2023 Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace OCC\Basics\DataStructures;
use InvalidArgumentException;
/**
* A type-sensitive, ordered Set.
*
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
* @package opencultureconsulting/basics
* @implements \Countable
* @implements \Iterator
* @implements \Serializable
*/
class Set extends AbstractList
{
/**
* Add a single item.
*
* @param mixed $item The item to add
* @param ?int $offset Optional offset to add, defaults to append
*
* @return void
*
* @throws \InvalidArgumentException
*/
public function add(mixed $item, ?int $offset = null): void
{
if (is_null($offset)) {
$this->append($item);
} elseif (isset($item)) {
if (!$this->isAllowedType($item)) {
throw new InvalidArgumentException('Parameter 1 must be an allowed type, ' . get_debug_type($item) . ' given.');
}
array_splice($this->items, $offset, 0, [$item]);
if ($offset >= 0) {
if ($offset <= $this->position) {
++$this->position;
}
} elseif ($offset < 0) {
if (($offset + $this->count()) <= $this->position) {
++$this->position;
}
}
}
}
/**
* Get a single item.
*
* @param ?int $offset Optional offset to peek, defaults to current
*
* @return mixed The item or NULL if empty
*/
public function peek(?int $offset = null): mixed
{
if (is_null($offset)) {
return $this->current();
}
$item = array_slice($this->items, $offset, 1);
return $item[0] ?? null;
}
/**
* Remove a single item.
*
* @param ?int $offset Optional offset to remove, defauls to current
*
* @return mixed The removed item or NULL if empty
*/
public function remove(?int $offset = null): mixed
{
if (is_null($offset)) {
$offset = $this->position;
}
$item = array_splice($this->items, $offset, 1);
if (isset($item[0])) {
if ($offset >= 0) {
if ($offset <= $this->position) {
--$this->position;
}
} elseif ($offset < 0) {
if (($offset + $this->count()) <= $this->position) {
--$this->position;
}
}
}
return $item[0] ?? null;
}
}

View File

@ -22,35 +22,25 @@ declare(strict_types=1);
namespace OCC\Basics\DataStructures; namespace OCC\Basics\DataStructures;
use Countable;
use InvalidArgumentException; use InvalidArgumentException;
use Iterator; use SplDoublyLinkedList;
use OCC\Basics\Traits\Getter; use OCC\Basics\Traits\Getter;
use Serializable;
/** /**
* A prototype for a type-sensitive, ordered list of items. * A type-sensitive, taversable List.
* *
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com> * @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
* @package opencultureconsulting/basics * @package opencultureconsulting/basics
*
* @implements \ArrayAccess
* @implements \Countable * @implements \Countable
* @implements \Iterator * @implements \Iterator
* @implements \Serializable * @implements \Serializable
*/ */
abstract class AbstractList implements Countable, Iterator, Serializable class StrictList extends SplDoublyLinkedList
{ {
use Getter; use Getter;
/**
* The items.
*/
protected array $items = [];
/**
* Current position of iterator.
*/
protected int $position = 0;
/** /**
* Defines the allowed types for items. * Defines the allowed types for items.
* If empty, all types are allowed. * If empty, all types are allowed.
@ -74,9 +64,28 @@ abstract class AbstractList implements Countable, Iterator, Serializable
protected array $allowedTypes = []; protected array $allowedTypes = [];
/** /**
* Append items. * Add/insert a new item at the specified index.
* @see SplDoublyLinkedList::add
* *
* @param mixed ...$items One or more items to add * @param int $index The index where the new item is to be inserted
* @param mixed $item The new item for the index
*
* @return void
*
* @throws \InvalidArgumentException
*/
public function add(int $index, mixed $item): void
{
if (!$this->isAllowedType($item)) {
throw new InvalidArgumentException('Parameter 2 must be an allowed type, ' . get_debug_type($item) . ' given.');
}
parent::add($index, $item);
}
/**
* Append items at the end of the list.
*
* @param mixed ...$items One or more items to append
* *
* @return void * @return void
* *
@ -84,57 +93,34 @@ abstract class AbstractList implements Countable, Iterator, Serializable
*/ */
public function append(mixed ...$items): void public function append(mixed ...$items): void
{ {
if (!empty($this->allowedTypes)) {
foreach ($items as $count => $item) { foreach ($items as $count => $item) {
if (!$this->isAllowedType($item)) { if (!$this->isAllowedType($item)) {
throw new InvalidArgumentException('Parameter ' . $count + 1 . ' must be an allowed type, ' . get_debug_type($item) . ' given.'); throw new InvalidArgumentException('Parameter ' . $count + 1 . ' must be an allowed type, ' . get_debug_type($item) . ' given.');
} }
} }
foreach ($items as $item) {
parent::push($item);
} }
$this->items = array_merge($this->items, $items);
} }
/** /**
* Get the number of items. * Get the allowed item types.
* @see Countable::count
* *
* @return int The number of items * @return array The list of allowed item types
*/ */
public function count(): int public function getAllowedTypes(): array
{ {
return count($this->items); return $this->allowedTypes;
} }
/** /**
* Clear all items. * Check if item is an allowed type.
*
* @return void
*/
public function clear(): void
{
$this->items = [];
$this->rewind();
}
/**
* Get the current item.
* @see Iterator::current
*
* @return mixed The current item or NULL if empty
*/
public function current(): mixed
{
return $this->items[$this->position] ?? null;
}
/**
* Check if an item is an allowed type.
* *
* @param mixed $item The item to check * @param mixed $item The item to check
* *
* @return bool Whether the item is an allowed type * @return bool Whether the item is an allowed type
*/ */
protected function isAllowedType(mixed $item): bool public function isAllowedType(mixed $item): bool
{ {
if (empty($this->allowedTypes)) { if (empty($this->allowedTypes)) {
return true; return true;
@ -152,17 +138,6 @@ abstract class AbstractList implements Countable, Iterator, Serializable
return false; return false;
} }
/**
* Get the current iterator position.
* @see Iterator::key
*
* @return int The current iterator position
*/
public function key(): int
{
return $this->position;
}
/** /**
* Magic getter method for $this->allowedTypes. * Magic getter method for $this->allowedTypes.
* @see OCC\Basics\Traits\Getter * @see OCC\Basics\Traits\Getter
@ -175,34 +150,70 @@ abstract class AbstractList implements Countable, Iterator, Serializable
} }
/** /**
* Move iterator to next position. * Set the item at the specified index.
* @see Iterator::next * @see ArrayAccess::offsetSet
*
* @param ?int $index The index being set or NULL to append
* @param mixed $item The new item for the index
* *
* @return void * @return void
*
* @throws \InvalidArgumentException
*/ */
public function next(): void public function offsetSet(mixed $index, mixed $item): void
{ {
++$this->position; if (!$this->isAllowedType($item)) {
throw new InvalidArgumentException('Parameter 2 must be an allowed type, ' . get_debug_type($item) . ' given.');
}
parent::offsetSet($index, $item);
} }
/** /**
* Reset the iterator position. * Prepend items at the start of the list.
* @see Iterator::rewind *
* @param mixed ...$items One or more items to prepend
* *
* @return void * @return void
*
* @throws \InvalidArgumentException
*/ */
public function rewind(): void public function prepend(mixed ...$items): void
{ {
$this->position = 0; foreach ($items as $count => $item) {
if (!$this->isAllowedType($item)) {
throw new InvalidArgumentException('Parameter ' . $count + 1 . ' must be an allowed type, ' . get_debug_type($item) . ' given.');
}
}
foreach ($items as $item) {
parent::unshift($item);
}
}
/**
* Push an item at the end of the list.
* @see SplDoublyLinkedList::push
*
* @param mixed $item The item to push
*
* @return void
*
* @throws \InvalidArgumentException
*/
public function push(mixed $item): void
{
if (!$this->isAllowedType($item)) {
throw new InvalidArgumentException('Parameter 1 must be an allowed type, ' . get_debug_type($item) . ' given.');
}
parent::push($item);
} }
/** /**
* Get string representation of $this. * Get string representation of $this.
* @see Serializable::serialize * @see Serializable::serialize
* *
* @return ?string String representation * @return string String representation
*/ */
public function serialize(): ?string public function serialize(): string
{ {
return serialize($this->__serialize()); return serialize($this->__serialize());
} }
@ -221,30 +232,27 @@ abstract class AbstractList implements Countable, Iterator, Serializable
} }
/** /**
* Check if there is an item at the current position. * Prepend the list with an item.
* @see Iterator::valid * @see SplDoublyLinkedList::unshift
* *
* @return bool Is there an item at the current position? * @param mixed $item The item to unshift
*/
public function valid(): bool
{
return isset($this->items[$this->position]);
}
/**
* Reset iterator position after cloning.
* *
* @return void * @return void
*
* @throws \InvalidArgumentException
*/ */
public function __clone(): void public function unshift(mixed $item): void
{ {
$this->rewind(); if (!$this->isAllowedType($item)) {
throw new InvalidArgumentException('Parameter 1 must be an allowed type, ' . get_debug_type($item) . ' given.');
}
parent::unshift($item);
} }
/** /**
* Create a type-sensitive traversable list of items. * Create a type-sensitive, traversable list of items.
* *
* @param iterable $items Initial list of items * @param iterable $items Initial set of items
* @param string[] $allowedTypes Allowed types of items (optional) * @param string[] $allowedTypes Allowed types of items (optional)
* *
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
@ -256,7 +264,6 @@ abstract class AbstractList implements Countable, Iterator, Serializable
} }
$this->allowedTypes = $allowedTypes; $this->allowedTypes = $allowedTypes;
$this->append(...$items); $this->append(...$items);
$this->rewind();
} }
/** /**
@ -278,7 +285,8 @@ abstract class AbstractList implements Countable, Iterator, Serializable
{ {
return [ return [
'allowedTypes' => $this->allowedTypes, 'allowedTypes' => $this->allowedTypes,
'items' => $this->items 'splDoublyLinkedList::flags' => $this->getIteratorMode(),
'splDoublyLinkedList::dllist' => iterator_to_array($this)
]; ];
} }
@ -291,6 +299,7 @@ abstract class AbstractList implements Countable, Iterator, Serializable
*/ */
public function __unserialize(array $data): void public function __unserialize(array $data): void
{ {
$this->__construct($data['items'], $data['allowedTypes']); $this->__construct($data['splDoublyLinkedList::dllist'], $data['allowedTypes']);
$this->setIteratorMode($data['splDoublyLinkedList::flags']);
} }
} }

View File

@ -22,52 +22,48 @@ declare(strict_types=1);
namespace OCC\Basics\DataStructures; namespace OCC\Basics\DataStructures;
use RuntimeException;
/** /**
* A type-sensitive, destructive Last In, First Out Stack. * A type-sensitive, taversable First In, First Out Queue (FIFO).
* *
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com> * @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
* @package opencultureconsulting/basics * @package opencultureconsulting/basics
*
* @implements \ArrayAccess
* @implements \Countable * @implements \Countable
* @implements \Iterator * @implements \Iterator
* @implements \Serializable * @implements \Serializable
*/ */
class Stack extends AbstractList class StrictQueue extends StrictList
{ {
/** /**
* Get the last item and remove it. * Set the mode of iteration.
* @see Iterator::current * @see SplDoublyLinkedList::setIteratorMode
* *
* @return mixed The last item or NULL if empty * @param int $mode The new iterator mode (0 or 1)
*
* @return int The set of flags and modes of iteration
*
* @throws \RuntimeException
*/ */
public function current(): mixed public function setIteratorMode(int $mode): int
{ {
return array_pop($this->items); if ($mode > 1) {
throw new RuntimeException('Changing the iterator direction of ' . static::class . ' is prohibited.');
}
return parent::setIteratorMode($mode);
} }
/** /**
* Get a single item without removing it. * Create a type-sensitive, traversable queue of items.
* *
* @param ?int $offset Optional offset to peek, defaults to last * @param iterable $items Initial set of items
* * @param string[] $allowedTypes Allowed types of items (optional)
* @return mixed The item or NULL if empty
*/ */
public function peek(?int $offset = null): mixed public function __construct(iterable $items = [], array $allowedTypes = [])
{ {
if (is_null($offset)) { parent::__construct($items, $allowedTypes);
return end($this->items) ?? null; $this->setIteratorMode(0);
}
$item = array_slice($this->items, $offset, 1);
return $item[0] ?? null;
}
/**
* Check if there is an item left on the stack.
* @see Iterator::valid
*
* @return bool Is there an item on the stack?
*/
public function valid(): bool
{
return (bool) $this->count();
} }
} }

View File

@ -22,52 +22,48 @@ declare(strict_types=1);
namespace OCC\Basics\DataStructures; namespace OCC\Basics\DataStructures;
use RuntimeException;
/** /**
* A type-sensitive, destructive First In, First Out Queue. * A type-sensitive, taversable Last In, First Out Stack (LIFO).
* *
* @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com> * @author Sebastian Meyer <sebastian.meyer@opencultureconsulting.com>
* @package opencultureconsulting/basics * @package opencultureconsulting/basics
*
* @implements \ArrayAccess
* @implements \Countable * @implements \Countable
* @implements \Iterator * @implements \Iterator
* @implements \Serializable * @implements \Serializable
*/ */
class Queue extends AbstractList class StrictStack extends StrictList
{ {
/** /**
* Get the first item and remove it. * Set the mode of iteration.
* @see Iterator::current * @see SplDoublyLinkedList::setIteratorMode
* *
* @return mixed The first item or NULL if empty * @param int $mode The new iterator mode (2 or 3)
*
* @return int The set of flags and modes of iteration
*
* @throws \RuntimeException
*/ */
public function current(): mixed public function setIteratorMode(int $mode): int
{ {
return array_shift($this->items); if ($mode < 2) {
throw new RuntimeException('Changing the iterator direction of ' . static::class . ' is prohibited.');
}
return parent::setIteratorMode($mode);
} }
/** /**
* Get a single item without removing it. * Create a type-sensitive, traversable stack of items.
* *
* @param ?int $offset Optional offset to peek, defaults to first * @param iterable $items Initial set of items
* * @param string[] $allowedTypes Allowed types of items (optional)
* @return mixed The item or NULL if empty
*/ */
public function peek(?int $offset = null): mixed public function __construct(iterable $items = [], array $allowedTypes = [])
{ {
if (is_null($offset)) { parent::__construct($items, $allowedTypes);
return reset($this->items) ?? null; $this->setIteratorMode(2);
}
$item = array_slice($this->items, $offset, 1);
return $item[0] ?? null;
}
/**
* Check if there is an item left on the queue.
* @see Iterator::valid
*
* @return bool Is there an item on the queue?
*/
public function valid(): bool
{
return (bool) $this->count();
} }
} }