From 0b3e23ebdc0567fd7727a07bc597da5c7ce2ca5e Mon Sep 17 00:00:00 2001 From: Sebastian Meyer Date: Tue, 7 Nov 2023 23:45:51 +0100 Subject: [PATCH] Add StrictQueue and StrictStack data structures --- src/DataStructures/AbstractStrictList.php | 298 +++++++++++++++++++++ src/DataStructures/Queue.php | 303 ---------------------- src/DataStructures/StrictQueue.php | 84 ++++++ src/DataStructures/StrictStack.php | 84 ++++++ 4 files changed, 466 insertions(+), 303 deletions(-) create mode 100644 src/DataStructures/AbstractStrictList.php delete mode 100644 src/DataStructures/Queue.php create mode 100644 src/DataStructures/StrictQueue.php create mode 100644 src/DataStructures/StrictStack.php diff --git a/src/DataStructures/AbstractStrictList.php b/src/DataStructures/AbstractStrictList.php new file mode 100644 index 0000000..5921eab --- /dev/null +++ b/src/DataStructures/AbstractStrictList.php @@ -0,0 +1,298 @@ + + * + * 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 . + */ + +declare(strict_types=1); + +namespace OCC\Basics\DataStructures; + +use Countable; +use InvalidArgumentException; +use Iterator; +use OCC\Basics\Traits\Getter; +use Serializable; + +/** + * Abstract class for a type-sensitive list of items. + * + * @author Sebastian Meyer + * @package opencultureconsulting/basics + * @implements \Countable + * @implements \Iterator + * @implements \Serializable + */ +abstract class AbstractStrictList implements Countable, Iterator, Serializable +{ + use Getter; + + /** + * The items. + */ + protected array $items = []; + + /** + * Count of iterations. + */ + protected int $iterations = 0; + + /** + * Defines the allowed types for items. + * If empty, all types are allowed. + * Possible values are: + * - "array" + * - "bool" + * - "callable" + * - "countable" + * - "float" or "double" + * - "int" or "integer" or "long" + * - "iterable" + * - "null" + * - "numeric" + * - "object" or FQCN + * - "resource" + * - "scalar" + * - "string" + * Fully qualified class names (FQCN) can be specified instead of the + * generic type "object". + */ + protected array $allowedTypes = []; + + /** + * Append items. + * + * @param mixed ...$items One or more items to add + * + * @return void + */ + public function append(mixed ...$items): void + { + if (!empty($this->allowedTypes)) { + foreach ($items as $count => $item) { + if (!$this->isAllowedType($item)) { + throw new InvalidArgumentException('Item ' . $count + 1 . ' must be an allowed type, ' . get_debug_type($item) . ' given.'); + } + } + } + $this->items = array_merge($this->items, $items); + } + + /** + * Get the number of items. + * @see Countable::count + * + * @return int The number of items + */ + public function count(): int + { + return count($this->items); + } + + /** + * Clear all items. + * + * @return void + */ + public function clear(): void + { + $this->items = []; + $this->rewind(); + } + + /** + * Get and remove the current item. + * @see Iterator::current + * + * @return mixed The current item or NULL if empty + */ + abstract public function current(): mixed; + + /** + * Check if an item is an allowed type. + * + * @param mixed $item The item to check + * + * @return bool Whether the item is an allowed type + */ + protected function isAllowedType(mixed $item): bool + { + if (empty($this->allowedTypes)) { + return true; + } + foreach ($this->allowedTypes as $type) { + $function = 'is_' . $type; + $fqcn = '\\' . ltrim($type, '\\'); + if (function_exists($function) && $function($item)) { + return true; + } + if (is_object($item) && is_a($item, $fqcn)) { + return true; + } + } + return false; + } + + /** + * Get the number of the current iteration. + * @see Iterator::key + * + * @return int The number of the current iteration + */ + public function key(): int + { + return $this->iterations; + } + + /** + * Magic getter method for $this->allowedTypes. + * @see OCC\Basics\Traits\Getter + * + * @return array The list of allowed item types + */ + protected function magicGetAllowedTypes(): array + { + return $this->allowedTypes; + } + + /** + * Count the next iteration. + * @see Iterator::next + * + * @return void + */ + public function next(): void + { + ++$this->iterations; + } + + /** + * Get the current item without removing it. + * + * @return mixed The current item or NULL if empty + */ + abstract public function peek(): mixed; + + /** + * Reset the iterator position. + * @see Iterator::rewind + * + * @return void + */ + public function rewind(): void + { + $this->iterations = 0; + } + + /** + * Get string representation of $this. + * @see Serializable::serialize + * + * @return ?string String representation + */ + public function serialize(): ?string + { + return serialize($this->__serialize()); + } + + /** + * Restore $this from string representation. + * @see Serializable::unserialize + * + * @param string $data String representation + * + * @return void + */ + public function unserialize($data): void + { + $this->__unserialize(unserialize($data)); + } + + /** + * Check if there are any items. + * @see Iterator::valid + * + * @return bool Whether there are items + */ + public function valid(): bool + { + return (bool) $this->items; + } + + /** + * Reset iteration counter after cloning. + * + * @return void + */ + public function __clone(): void + { + $this->rewind(); + } + + /** + * Create a (type-sensitive) traversable set of items. + * + * @param array $items Initial set of items + * @param string[] $allowedTypes Allowed types of items (optional) + */ + public function __construct(array $items = [], array $allowedTypes = []) + { + if (!empty($allowedTypes)) { + if (array_sum(array_map('is_string', $allowedTypes)) !== count($allowedTypes)) { + throw new InvalidArgumentException('Allowed types must be array of strings or empty array.'); + } + $this->allowedTypes = $allowedTypes; + } + $this->append(...$items); + } + + /** + * Get debug information for $this. + * + * @return array Array of debug information + */ + public function __debugInfo(): array + { + return $this->__serialize(); + } + + /** + * Get array representation of $this. + * + * @return array Array representation + */ + public function __serialize(): array + { + return [ + 'allowedTypes' => $this->allowedTypes, + 'items' => $this->items + ]; + } + + /** + * Restore $this from array representation. + * + * @param array $data Array representation + * + * @return void + */ + public function __unserialize(array $data): void + { + $this->allowedTypes = $data['allowedTypes'] ?? []; + $this->items = $data['items'] ?? []; + } +} diff --git a/src/DataStructures/Queue.php b/src/DataStructures/Queue.php deleted file mode 100644 index bc98916..0000000 --- a/src/DataStructures/Queue.php +++ /dev/null @@ -1,303 +0,0 @@ - - * - * 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 . - */ - -declare(strict_types=1); - -namespace OCC\Basics\DataStructures; - -use ArrayAccess; -use Countable; -use InvalidArgumentException; -use OCC\Basics\Traits\Getter; -use OutOfBoundsException; -use OutOfRangeException; -use SeekableIterator; - -/** - * Handles an ordered queue of items - optionally type-sensitive. - * - * @author Sebastian Meyer - * @package opencultureconsulting/basics - * @implements ArrayAccess - * @implements Countable - * @implements SeekableIterator - */ -class Queue implements ArrayAccess, Countable, SeekableIterator -{ - use Getter; - - /** - * Holds the queue's elements. - */ - protected array $queue = []; - - /** - * Holds the queue's current index. - */ - protected int $index = 0; - - /** - * Defines the allowed types for the queue's elements. - * If empty, all types are allowed. - * Possible values are: - * - "array" - * - "bool" - * - "callable" - * - "countable" - * - "float" or "double" - * - "int" or "integer" or "long" - * - "iterable" - * - "null" - * - "numeric" - * - "object" or FQCN - * - "resource" - * - "scalar" - * - "string" - * Fully qualified class names (FQCN) can be specified instead of the - * generic type "object". - */ - protected array $allowedTypes = []; - - /** - * Check if a variable is an allowed type. - * - * @param mixed $var The variable to check - * - * @return bool Whether the variable is an allowed type - */ - protected function isAllowedType(mixed $var): bool - { - if (empty($this->allowedTypes)) { - return true; - } - foreach ($this->allowedTypes as $type) { - $function = 'is_' . $type; - $fqcn = '\\' . ltrim($type, '\\'); - if (function_exists($function) && $function($var)) { - return true; - } - if (is_object($var) && is_a($var, $fqcn)) { - return true; - } - } - return false; - } - - /** - * Checks if a given index is in the range of valid indexes. - * - * @param mixed $offset The index to check - * @param bool $allowAppend Should the next free index be valid as well? - * - * @return bool Whether the given index is in valid range - */ - protected function isIndexInRange(mixed $offset, bool $allowAppend = false): bool - { - $options = [ - 'options' => [ - 'min_range' => 0, - 'max_range' => count($this->queue) - ($allowAppend ? 0 : 1) - ] - ]; - return filter_var($offset, FILTER_VALIDATE_INT, $options) !== false; - } - - /** - * Check if a given index exists on the queue. - * @see ArrayAccess::offsetExists - * - * @param int $offset The queue's index to check - * - * @return bool Whether the given index is valid - */ - public function offsetExists(mixed $offset): bool - { - return isset($this->queue[$offset]); - } - - /** - * Get the element with given index from the queue. - * @see ArrayAccess::offsetGet - * - * @param int $offset The queue's index to get - * - * @return ?mixed The queue's element at given index or NULL - */ - public function offsetGet(mixed $offset): mixed - { - return $this->queue[$offset] ?? null; - } - - /** - * Set the element at given index in the queue. - * @see ArrayAccess::offsetSet - * - * @param ?int $offset The queue's index to set or NULL to append - * Must be between 0 and the length of the queue. - * @param mixed $value The element to set at the given index - * Must be of an allowed type if applicable. - * - * @return void - * - * @throws InvalidArgumentException - * @throws OutOfRangeException - */ - public function offsetSet(mixed $offset, mixed $value): void - { - if (is_null($offset)) { - $offset = count($this->queue); - } elseif (!$this->isIndexInRange($offset, true)) { - throw new OutOfRangeException('Index must be an integer between 0 and the length of the queue (' . count($this->queue) . ') or NULL to append: ' . get_debug_type($offset) . (is_int($offset) ? ' ' . (string) $offset : '') . ' given.'); - } - if (!$this->isAllowedType($value)) { - throw new InvalidArgumentException('Value must be one of ' . implode(', ', $this->allowedTypes) . ': ' . get_debug_type($value) . ' given.'); - } - $this->queue[(int) $offset] = $value; - } - - /** - * Remove the element with given index from the queue. - * @see ArrayAccess::offsetUnset - * - * @param int $offset The queue's index to unset - * - * @return void - */ - public function offsetUnset(mixed $offset): void - { - if ($this->isIndexInRange($offset)) { - array_splice($this->queue, (int) $offset, 1); - if ((int) $offset <= $this->index) { - --$this->index; - } - } - } - - /** - * Get the number of elements in the queue. - * @see Countable::count - * - * @return int The number of items in the queue - */ - public function count(): int - { - return count($this->queue); - } - - /** - * Get the current element from the queue. - * @see Iterator::current - * - * @return mixed|null The queue's current element or NULL - */ - public function current(): mixed - { - return $this->queue[$this->index] ?? null; - } - - /** - * Get the current index from the queue. - * @see Iterator::key - * - * @return int The queue's current index - */ - public function key(): int - { - return $this->index; - } - - /** - * Move the index to next element of the queue. - * @see Iterator::next - * - * @return void - */ - public function next(): void - { - ++$this->index; - } - - /** - * Reset the index to the first element of the queue. - * @see Iterator::rewind - * - * @return void - */ - public function rewind(): void - { - $this->index = 0; - } - - /** - * Check if the queue's current index is valid. - * @see Iterator::valid - * - * @return bool Whether the queue's current index is valid - */ - public function valid(): bool - { - return isset($this->queue[$this->index]); - } - - /** - * Sets the queue's current index. - * @see SeekableIterator::seek - * - * @param int $offset The queue's new index - * - * @return void - * - * @throws OutOfBoundsException - */ - public function seek(int $offset): void - { - if (!$this->isIndexInRange($offset)) { - throw new OutOfBoundsException('Index must be an integer between 0 and the length of the queue (' . count($this->queue) . '): ' . (string) $offset . ' given.'); - } - $this->index = $offset; - } - - /** - * Magic getter method for $this->allowedTypes. - * @see OCC\Traits\Getter - * - * @return array The list of the queue's allowed element types - */ - protected function magicGetAllowedTypes(): array - { - return $this->allowedTypes; - } - - /** - * Create a (type-sensitive) queue of elements. - * @see OCC\Helper\Queue::allowedTypes - * - * @param string[] $allowedTypes Allowed types of queue's elements - * - * @throws InvalidArgumentException - */ - public function __construct(array $allowedTypes = []) - { - if (array_sum(array_map('is_string', $allowedTypes)) !== count($allowedTypes)) { - throw new InvalidArgumentException('Parameter 1 must be array of strings or empty array.'); - } - $this->allowedTypes = $allowedTypes; - } -} diff --git a/src/DataStructures/StrictQueue.php b/src/DataStructures/StrictQueue.php new file mode 100644 index 0000000..7fad134 --- /dev/null +++ b/src/DataStructures/StrictQueue.php @@ -0,0 +1,84 @@ + + * + * 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 . + */ + +declare(strict_types=1); + +namespace OCC\Basics\DataStructures; + +use Countable; +use Iterator; +use Serializable; + +/** + * A (type-sensitive) destructive First In, First Out Queue. + * + * @author Sebastian Meyer + * @package opencultureconsulting/basics + * @implements \Countable + * @implements \Iterator + * @implements \Serializable + */ +class StrictQueue extends AbstractStrictList +{ + /** + * Get and remove the first item. + * @see Iterator::current + * + * @return mixed The first item or NULL if empty + */ + public function current(): mixed + { + return array_shift($this->items); + } + + /** + * Dequeue the first item. + * Alias of Queue::current + * + * @return mixed The first item or NULL if empty + */ + public function dequeue(): mixed + { + return $this->current(); + } + + /** + * Enqueue items. + * Alias of Queue::append + * + * @param mixed ...$items One or more items to add + * + * @return void + */ + public function enqueue(mixed ...$items): void + { + $this->append(...$items); + } + + /** + * Get the first item without removing it. + * + * @return mixed The first item or NULL if empty + */ + public function peek(): mixed + { + return $this->items[array_key_first($this->items)] ?? null; + } +} diff --git a/src/DataStructures/StrictStack.php b/src/DataStructures/StrictStack.php new file mode 100644 index 0000000..5a21813 --- /dev/null +++ b/src/DataStructures/StrictStack.php @@ -0,0 +1,84 @@ + + * + * 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 . + */ + +declare(strict_types=1); + +namespace OCC\Basics\DataStructures; + +use Countable; +use Iterator; +use Serializable; + +/** + * A (type-sensitive) destructive Last In, First Out Stack. + * + * @author Sebastian Meyer + * @package opencultureconsulting/basics + * @implements \Countable + * @implements \Iterator + * @implements \Serializable + */ +class StrictStack extends AbstractStrictList +{ + /** + * Get and remove the last item. + * @see Iterator::current + * + * @return mixed The last item or NULL if empty + */ + public function current(): mixed + { + return array_pop($this->items); + } + + /** + * Get the last item without removing it. + * + * @return mixed The first item or NULL if empty + */ + public function peek(): mixed + { + return $this->items[array_key_last($this->items)] ?? null; + } + + /** + * Stack items. + * Alias of Stack::append + * + * @param mixed ...$items One or more items to add + * + * @return void + */ + public function stack(mixed ...$items): void + { + $this->append(...$items); + } + + /** + * Unstack the last item. + * Alias of Stack::current + * + * @return mixed The last item or NULL if empty + */ + public function unstack(): mixed + { + return $this->current(); + } +}