From 86d0e8983723a119e7babbb8e317cb1275187a7b Mon Sep 17 00:00:00 2001
From: Lkeme <19500576+lkeme@users.noreply.github.com>
Date: Tue, 18 Feb 2025 18:49:19 +0800
Subject: [PATCH] -
---
composer.json | 20 +-
packages/flintstone/composer.json | 2 +-
packages/php-ds/.gitattributes | 4 +
packages/php-ds/.github/workflows/ci.yaml | 67 ++
packages/php-ds/.gitignore | 4 +
packages/php-ds/CHANGELOG.md | 41 +
packages/php-ds/CONTRIBUTING.md | 21 +
packages/php-ds/LICENSE | 20 +
packages/php-ds/README.md | 38 +
packages/php-ds/composer.json | 33 +
packages/php-ds/phpunit.xml | 25 +
packages/php-ds/src/Collection.php | 56 ++
packages/php-ds/src/Deque.php | 31 +
packages/php-ds/src/Hashable.php | 32 +
packages/php-ds/src/Map.php | 812 ++++++++++++++++++
packages/php-ds/src/Pair.php | 158 ++++
packages/php-ds/src/PriorityQueue.php | 340 ++++++++
packages/php-ds/src/Queue.php | 197 +++++
packages/php-ds/src/Sequence.php | 331 +++++++
packages/php-ds/src/Set.php | 541 ++++++++++++
packages/php-ds/src/Stack.php | 200 +++++
packages/php-ds/src/Traits/Capacity.php | 130 +++
.../php-ds/src/Traits/GenericCollection.php | 81 ++
.../php-ds/src/Traits/GenericSequence.php | 447 ++++++++++
.../php-ds/src/Traits/SquaredCapacity.php | 58 ++
packages/php-ds/src/Vector.php | 37 +
packages/php-ds/tests/bootstrap.php | 7 +
27 files changed, 3724 insertions(+), 9 deletions(-)
create mode 100644 packages/php-ds/.gitattributes
create mode 100644 packages/php-ds/.github/workflows/ci.yaml
create mode 100644 packages/php-ds/.gitignore
create mode 100644 packages/php-ds/CHANGELOG.md
create mode 100644 packages/php-ds/CONTRIBUTING.md
create mode 100644 packages/php-ds/LICENSE
create mode 100644 packages/php-ds/README.md
create mode 100644 packages/php-ds/composer.json
create mode 100644 packages/php-ds/phpunit.xml
create mode 100644 packages/php-ds/src/Collection.php
create mode 100644 packages/php-ds/src/Deque.php
create mode 100644 packages/php-ds/src/Hashable.php
create mode 100644 packages/php-ds/src/Map.php
create mode 100644 packages/php-ds/src/Pair.php
create mode 100644 packages/php-ds/src/PriorityQueue.php
create mode 100644 packages/php-ds/src/Queue.php
create mode 100644 packages/php-ds/src/Sequence.php
create mode 100644 packages/php-ds/src/Set.php
create mode 100644 packages/php-ds/src/Stack.php
create mode 100644 packages/php-ds/src/Traits/Capacity.php
create mode 100644 packages/php-ds/src/Traits/GenericCollection.php
create mode 100644 packages/php-ds/src/Traits/GenericSequence.php
create mode 100644 packages/php-ds/src/Traits/SquaredCapacity.php
create mode 100644 packages/php-ds/src/Vector.php
create mode 100644 packages/php-ds/tests/bootstrap.php
diff --git a/composer.json b/composer.json
index eb55431..0b41215 100644
--- a/composer.json
+++ b/composer.json
@@ -23,28 +23,31 @@
},
"repositories": [
{
- "description": "Development package",
+ "description": "fire015/flintstone",
"type": "path",
"url": "packages/flintstone",
"canonical": false
},
+ {
+ "description": "php-ds/php-ds",
+ "type": "path",
+ "url": "packages/php-ds",
+ "canonical": false
+ },
{
"description": "PhpComposer",
"type": "composer",
- "url": "https://packagist.phpcomposer.com",
- "canonical": false
+ "url": "https://packagist.phpcomposer.com"
},
{
"description": "TencentCloud",
"type": "composer",
- "url": "https://mirrors.tencent.com/composer/",
- "canonical": false
+ "url": "https://mirrors.tencent.com/composer/"
},
{
"description": "AliCloud",
"type": "composer",
- "url": "https://mirrors.aliyun.com/composer/",
- "canonical": false
+ "url": "https://mirrors.aliyun.com/composer/"
}
],
"require": {
@@ -63,11 +66,12 @@
"jbzoo/data": "dev-master",
"grasmash/expander": "dev-main",
"amphp/amp": "3.x-dev",
- "fire015/flintstone-fix-php-84": "dev-master",
"overtrue/pinyin": "dev-master",
"guzzlehttp/guzzle": "^7.0",
"toolkit/pflag": "dev-main",
"symfony/console": "^7.0",
+ "fire015/flintstone": "dev-master",
+ "php-ds/php-ds": "dev-master as v1.1",
"malios/php-to-ascii-table": "dev-master"
},
"autoload": {
diff --git a/packages/flintstone/composer.json b/packages/flintstone/composer.json
index eb4a4e7..d1edeec 100644
--- a/packages/flintstone/composer.json
+++ b/packages/flintstone/composer.json
@@ -1,5 +1,5 @@
{
- "name": "fire015/flintstone-fix-php-84",
+ "name": "fire015/flintstone",
"type": "library",
"description": "A key/value database store using flat files for PHP",
"keywords": ["flintstone", "database", "cache", "files", "memory"],
diff --git a/packages/php-ds/.gitattributes b/packages/php-ds/.gitattributes
new file mode 100644
index 0000000..f68aa39
--- /dev/null
+++ b/packages/php-ds/.gitattributes
@@ -0,0 +1,4 @@
+/.github export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/phpunit.xml export-ignore
diff --git a/packages/php-ds/.github/workflows/ci.yaml b/packages/php-ds/.github/workflows/ci.yaml
new file mode 100644
index 0000000..2551ffc
--- /dev/null
+++ b/packages/php-ds/.github/workflows/ci.yaml
@@ -0,0 +1,67 @@
+name: "CI"
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "master"
+ workflow_dispatch:
+
+jobs:
+ phpunit:
+ name: "PHPUnit"
+ runs-on: "ubuntu-20.04"
+
+ strategy:
+ matrix:
+ php-version:
+ - "7.4"
+ - "8.0"
+ - "8.1"
+ - "8.2"
+ - "8.3"
+ dependencies:
+ - "highest"
+
+ steps:
+ - name: "Checkout"
+ uses: "actions/checkout@v2"
+ with:
+ fetch-depth: 2
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ php-version: "${{ matrix.php-version }}"
+ coverage: "pcov"
+ ini-values: "zend.assertions=1"
+
+ - name: "Install dependencies with Composer"
+ uses: "ramsey/composer-install@v1"
+ with:
+ dependency-versions: "${{ matrix.dependencies }}"
+
+ - name: "Run PHPUnit"
+ run: "vendor/bin/phpunit"
+
+ upload_coverage:
+ name: "Upload coverage to Codecov"
+ runs-on: "ubuntu-20.04"
+ needs:
+ - "phpunit"
+
+ steps:
+ - name: "Checkout"
+ uses: "actions/checkout@v2"
+ with:
+ fetch-depth: 2
+
+ - name: "Download coverage files"
+ uses: "actions/download-artifact@v2"
+ with:
+ path: "reports"
+
+ - name: "Upload to Codecov"
+ uses: "codecov/codecov-action@v1"
+ with:
+ directory: reports
diff --git a/packages/php-ds/.gitignore b/packages/php-ds/.gitignore
new file mode 100644
index 0000000..a114817
--- /dev/null
+++ b/packages/php-ds/.gitignore
@@ -0,0 +1,4 @@
+.phpunit.result.cache
+build
+vendor
+composer.lock
\ No newline at end of file
diff --git a/packages/php-ds/CHANGELOG.md b/packages/php-ds/CHANGELOG.md
new file mode 100644
index 0000000..5248a79
--- /dev/null
+++ b/packages/php-ds/CHANGELOG.md
@@ -0,0 +1,41 @@
+# Change Log
+All notable changes to this project will be documented in this file.
+This project adheres to [Semantic Versioning](http://semver.org/).
+
+## Unreleased
+### Fixed
+- `Collection` PHPDoc now correctly states that it extends `IteratorAggregate`, rather than just `Traversable`.
+
+## [1.4.1] - 2022-03-09
+
+## [1.4.0] - 2021-11-17
+
+## [1.3.0] - 2020-10-13
+### Changed
+- Implement ArrayAccess consistently
+### Fixed
+- Return types were incorrectly nullable in some cases
+- Deque capacity was inconsistent with the extension
+
+## [1.2.0] - 2017-08-03
+### Changed
+- Minor capacity updates
+
+## [1.1.1] - 2016-08-09
+### Fixed
+- `Stack` and `Queue` array access should throw `OutOfBoundsException`, not `Error`.
+
+### Improved
+- Added a lot of docblock comments that were missing.
+
+## [1.1.0] - 2016-08-04
+### Added
+- `Pair::copy`
+
+## [1.0.3] - 2016-08-01
+### Added
+- `Set::merge`
+
+## [1.0.2] - 2016-07-31
+### Added
+- `Map::putAll`
diff --git a/packages/php-ds/CONTRIBUTING.md b/packages/php-ds/CONTRIBUTING.md
new file mode 100644
index 0000000..1bf340e
--- /dev/null
+++ b/packages/php-ds/CONTRIBUTING.md
@@ -0,0 +1,21 @@
+# Contributing
+
+Contributions are accepted via [pull requests](https://github.com/php-ds/ext/pulls). If you would like to report a bug, please create an [issue](https://github.com/php-ds/ext/issues) instead.
+
+## Issues
+
+- **How to reproduce** - Provide an easy way to reproduce the bug. This makes it easier for others to debug.
+
+- **Platform details** - Specify your platform and your PHP version, eg. "PHP 7.0.2 on Ubuntu 14.04 64x".
+
+## Pull Requests
+
+- **Add tests** - Your patch won't be accepted if it doesn't have tests where appropriate.
+
+- **Document any change in behaviour** - Make sure the README and any other relevant documentation updated.
+
+- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
+
+- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting.
+
+- **Coding style** - Try to match the style of the rest of the source wherever possible. Your patch won't be accepted if the style is significantly different.
diff --git a/packages/php-ds/LICENSE b/packages/php-ds/LICENSE
new file mode 100644
index 0000000..5f2a777
--- /dev/null
+++ b/packages/php-ds/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+Copyright (c) 2016 Rudi Theunissen
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
+THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/php-ds/README.md b/packages/php-ds/README.md
new file mode 100644
index 0000000..13f57a6
--- /dev/null
+++ b/packages/php-ds/README.md
@@ -0,0 +1,38 @@
+# Data Structures for PHP
+
+[](https://github.com/php-ds/polyfill/actions?query=workflow%3A%22CI%22+branch%3Amaster)
+[](https://packagist.org/packages/php-ds/php-ds)
+
+This is a compatibility polyfill for the [extension](https://github.com/php-ds/extension). You should include this package as a dependency of your project
+to ensure that your codebase would still be functional in an environment where the extension is not installed. The polyfill will not be loaded if the extension is installed and enabled.
+
+## Install
+
+```bash
+composer require php-ds/php-ds
+```
+
+You can also *require* that the extension be installed using `ext-ds`.
+
+## Test
+
+```
+composer install
+composer test
+```
+
+Make sure that the *ds* extension is not enabled, as the polyfill will not be loaded if it is.
+The test output will indicate whether the extension is active.
+
+## Contributing
+
+Please see [CONTRIBUTING](CONTRIBUTING.md) for more information.
+
+### Credits
+
+- [Rudi Theunissen](https://github.com/rtheunissen)
+- [Joe Watkins](https://github.com/krakjoe)
+
+### License
+
+The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
diff --git a/packages/php-ds/composer.json b/packages/php-ds/composer.json
new file mode 100644
index 0000000..f182555
--- /dev/null
+++ b/packages/php-ds/composer.json
@@ -0,0 +1,33 @@
+{
+ "name": "php-ds/php-ds",
+ "license": "MIT",
+ "description": "Specialized data structures as alternatives to the PHP array",
+ "keywords": ["php", "ds", "data structures", "polyfill"],
+ "authors": [
+ {
+ "name": "Rudi Theunissen",
+ "email": "rudolf.theunissen@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.4",
+ "ext-json": "*"
+ },
+ "require-dev": {
+ "php-ds/tests": "^1.5"
+ },
+ "provide": {
+ "ext-ds": "1.5.0"
+ },
+ "suggest": {
+ "ext-ds": "to improve performance and reduce memory usage"
+ },
+ "scripts": {
+ "test": "phpunit"
+ },
+ "autoload": {
+ "psr-4" : {
+ "Ds\\": "src"
+ }
+ }
+}
diff --git a/packages/php-ds/phpunit.xml b/packages/php-ds/phpunit.xml
new file mode 100644
index 0000000..3ddac77
--- /dev/null
+++ b/packages/php-ds/phpunit.xml
@@ -0,0 +1,25 @@
+
+
+
+ vendor/php-ds/tests/tests
+
+
+
+
+ src
+
+
+
+
+
+
+
+
diff --git a/packages/php-ds/src/Collection.php b/packages/php-ds/src/Collection.php
new file mode 100644
index 0000000..8ef0f06
--- /dev/null
+++ b/packages/php-ds/src/Collection.php
@@ -0,0 +1,56 @@
+
+ */
+interface Collection extends \IteratorAggregate, \Countable, \JsonSerializable
+{
+ /**
+ * Removes all values from the collection.
+ */
+ public function clear();
+
+ /**
+ * Returns the size of the collection.
+ *
+ * @return int
+ */
+ public function count(): int;
+
+ /**
+ * Returns a shallow copy of the collection.
+ *
+ * @return static a copy of the collection.
+ *
+ * @psalm-return static
+ */
+ public function copy();
+
+ /**
+ * Returns whether the collection is empty.
+ *
+ * This should be equivalent to a count of zero, but is not required.
+ * Implementations should define what empty means in their own context.
+ */
+ public function isEmpty(): bool;
+
+ /**
+ * Returns an array representation of the collection.
+ *
+ * The format of the returned array is implementation-dependent.
+ * Some implementations may throw an exception if an array representation
+ * could not be created.
+ *
+ * @return array
+ */
+ public function toArray(): array;
+}
diff --git a/packages/php-ds/src/Deque.php b/packages/php-ds/src/Deque.php
new file mode 100644
index 0000000..6c768e3
--- /dev/null
+++ b/packages/php-ds/src/Deque.php
@@ -0,0 +1,31 @@
+
+ * @template-use Traits\GenericCollection
+ * @template-use Traits\GenericSequence
+ */
+final class Deque implements Sequence
+{
+ use Traits\GenericCollection;
+ use Traits\GenericSequence;
+ use Traits\SquaredCapacity;
+
+ public const MIN_CAPACITY = 8;
+
+ protected function shouldIncreaseCapacity(): bool
+ {
+ return count($this) >= $this->capacity;
+ }
+}
diff --git a/packages/php-ds/src/Hashable.php b/packages/php-ds/src/Hashable.php
new file mode 100644
index 0000000..a98b986
--- /dev/null
+++ b/packages/php-ds/src/Hashable.php
@@ -0,0 +1,32 @@
+
+ * @implements \ArrayAccess
+ * @template-use Traits\GenericCollection
+ */
+final class Map implements Collection, \ArrayAccess
+{
+ use Traits\GenericCollection;
+ use Traits\SquaredCapacity;
+
+ public const MIN_CAPACITY = 8;
+
+ /**
+ * @var array internal array to store pairs
+ *
+ * @psalm-var array
+ */
+ private $pairs = [];
+
+ /**
+ * Creates a new instance.
+ *
+ * @param iterable $values
+ *
+ * @psalm-param iterable $values
+ */
+ public function __construct(iterable $values = [])
+ {
+ if (func_num_args()) {
+ $this->putAll($values);
+ }
+
+ }
+
+ /**
+ * Updates all values by applying a callback function to each value.
+ *
+ * @param callable $callback Accepts two arguments: key and value, should
+ * return what the updated value will be.
+ *
+ * @psalm-param callable(TKey, TValue): TValue $callback
+ */
+ public function apply(callable $callback)
+ {
+ foreach ($this->pairs as &$pair) {
+ $pair->value = $callback($pair->key, $pair->value);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clear()
+ {
+ $this->pairs = [];
+ $this->capacity = self::MIN_CAPACITY;
+ }
+
+ /**
+ * Return the first Pair from the Map
+ *
+ * @return Pair
+ *
+ * @throws UnderflowException
+ *
+ * @psalm-return Pair
+ */
+ public function first(): Pair
+ {
+ if ($this->isEmpty()) {
+ throw new UnderflowException();
+ }
+
+ return $this->pairs[0];
+ }
+
+ /**
+ * Return the last Pair from the Map
+ *
+ * @return Pair
+ *
+ * @throws UnderflowException
+ *
+ * @psalm-return Pair
+ */
+ public function last(): Pair
+ {
+ if ($this->isEmpty()) {
+ throw new UnderflowException();
+ }
+
+ return $this->pairs[count($this->pairs) - 1];
+ }
+
+ /**
+ * Return the pair at a specified position in the Map
+ *
+ * @return Pair
+ *
+ * @throws OutOfRangeException
+ *
+ * @psalm-return Pair
+ */
+ public function skip(int $position): Pair
+ {
+ if ($position < 0 || $position >= count($this->pairs)) {
+ throw new OutOfRangeException();
+ }
+
+ return $this->pairs[$position]->copy();
+ }
+
+ /**
+ * Returns the result of associating all keys of a given traversable object
+ * or array with their corresponding values, as well as those of this map.
+ *
+ * @param array|\Traversable $values
+ *
+ * @return Map
+ *
+ * @template TKey2
+ * @template TValue2
+ * @psalm-param iterable $values
+ * @psalm-return Map
+ */
+ public function merge($values): Map
+ {
+ $merged = new self($this);
+ $merged->putAll($values);
+ return $merged;
+ }
+
+ /**
+ * Creates a new map containing the pairs of the current instance whose keys
+ * are also present in the given map. In other words, returns a copy of the
+ * current map with all keys removed that are not also in the other map.
+ *
+ * @param Map $map The other map.
+ *
+ * @return Map A new map containing the pairs of the current instance
+ * whose keys are also present in the given map. In other
+ * words, returns a copy of the current map with all keys
+ * removed that are not also in the other map.
+ *
+ * @template TKey2
+ * @template TValue2
+ * @psalm-param Map $map
+ * @psalm-return Map
+ */
+ public function intersect(Map $map): Map
+ {
+ return $this->filter(function ($key) use ($map) {
+ return $map->hasKey($key);
+ });
+ }
+
+ /**
+ * Returns the result of removing all keys from the current instance that
+ * are present in a given map.
+ *
+ * @param Map $map The map containing the keys to exclude.
+ *
+ * @return Map The result of removing all keys from the current instance
+ * that are present in a given map.
+ *
+ * @template TValue2
+ * @psalm-param Map $map
+ * @psalm-return Map
+ */
+ public function diff(Map $map): Map
+ {
+ return $this->filter(function ($key) use ($map) {
+ return !$map->hasKey($key);
+ });
+ }
+
+ /**
+ * Determines whether two keys are equal.
+ *
+ * @param mixed $a
+ * @param mixed $b
+ *
+ * @psalm-param TKey $a
+ * @psalm-param TKey $b
+ */
+ private function keysAreEqual($a, $b): bool
+ {
+ if (is_object($a) && $a instanceof Hashable) {
+ return get_class($a) === get_class($b) && $a->equals($b);
+ }
+
+ return $a === $b;
+ }
+
+ /**
+ * Attempts to look up a key in the table.
+ *
+ * @param $key
+ *
+ * @return Pair|null
+ *
+ * @psalm-return Pair|null
+ */
+ private function lookupKey($key)
+ {
+ foreach ($this->pairs as $pair) {
+ if ($this->keysAreEqual($pair->key, $key)) {
+ return $pair;
+ }
+ }
+ }
+
+ /**
+ * Attempts to look up a value in the table.
+ *
+ * @param $value
+ *
+ * @return Pair|null
+ *
+ * @psalm-return Pair|null
+ */
+ private function lookupValue($value)
+ {
+ foreach ($this->pairs as $pair) {
+ if ($pair->value === $value) {
+ return $pair;
+ }
+ }
+ }
+
+ /**
+ * Returns whether an association a given key exists.
+ *
+ * @param mixed $key
+ *
+ * @psalm-param TKey $key
+ */
+ public function hasKey($key): bool
+ {
+ return $this->lookupKey($key) !== null;
+ }
+
+ /**
+ * Returns whether an association for a given value exists.
+ *
+ * @param mixed $value
+ *
+ * @psalm-param TValue $value
+ */
+ public function hasValue($value): bool
+ {
+ return $this->lookupValue($value) !== null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function count(): int
+ {
+ return count($this->pairs);
+ }
+
+ /**
+ * Returns a new map containing only the values for which a predicate
+ * returns true. A boolean test will be used if a predicate is not provided.
+ *
+ * @param callable|null $callback Accepts a key and a value, and returns:
+ * true : include the value,
+ * false: skip the value.
+ *
+ * @return Map
+ *
+ * @psalm-param (callable(TKey, TValue): bool)|null $callback
+ * @psalm-return Map
+ */
+ public function filter(callable|null $callback = null): Map
+ {
+ $filtered = new self();
+
+ foreach ($this as $key => $value) {
+ if ($callback ? $callback($key, $value) : $value) {
+ $filtered->put($key, $value);
+ }
+ }
+
+ return $filtered;
+ }
+
+ /**
+ * Returns the value associated with a key, or an optional default if the
+ * key is not associated with a value.
+ *
+ * @param mixed $key
+ * @param mixed $default
+ *
+ * @return mixed The associated value or fallback default if provided.
+ *
+ * @throws OutOfBoundsException if no default was provided and the key is
+ * not associated with a value.
+ *
+ * @template TDefault
+ * @psalm-param TKey $key
+ * @psalm-param TDefault $default
+ * @psalm-return TValue|TDefault
+ */
+ public function get($key, $default = null)
+ {
+ if (($pair = $this->lookupKey($key))) {
+ return $pair->value;
+ }
+
+ // Check if a default was provided.
+ if (func_num_args() === 1) {
+ throw new OutOfBoundsException();
+ }
+
+ return $default;
+ }
+
+ /**
+ * Returns a set of all the keys in the map.
+ *
+ * @return Set
+ *
+ * @psalm-return Set
+ */
+ public function keys(): Set
+ {
+ $key = function ($pair) {
+ return $pair->key;
+ };
+
+ return new Set(array_map($key, $this->pairs));
+ }
+
+ /**
+ * Returns a new map using the results of applying a callback to each value.
+ *
+ * The keys will be equal in both maps.
+ *
+ * @param callable $callback Accepts two arguments: key and value, should
+ * return what the updated value will be.
+ *
+ * @return Map
+ *
+ * @template TNewValue
+ * @psalm-param callable(TKey, TValue): TNewValue $callback
+ * @psalm-return Map
+ */
+ public function map(callable $callback): Map
+ {
+ $mapped = new self();
+ foreach ($this->pairs as $pair) {
+ $mapped->put($pair->key, $callback($pair->key, $pair->value));
+ }
+
+ return $mapped;
+ }
+
+ /**
+ * Returns a sequence of pairs representing all associations.
+ *
+ * @return Sequence
+ *
+ * @psalm-return Sequence>
+ */
+ public function pairs(): Sequence
+ {
+ $copy = function ($pair) {
+ return $pair->copy();
+ };
+
+ return new Vector(array_map($copy, $this->pairs));
+ }
+
+ /**
+ * Associates a key with a value, replacing a previous association if there
+ * was one.
+ *
+ * @param mixed $key
+ * @param mixed $value
+ *
+ * @psalm-param TKey $key
+ * @psalm-param TValue $value
+ */
+ public function put($key, $value)
+ {
+ $pair = $this->lookupKey($key);
+
+ if ($pair) {
+ $pair->value = $value;
+
+ } else {
+ $this->checkCapacity();
+ $this->pairs[] = new Pair($key, $value);
+ }
+ }
+
+ /**
+ * Creates associations for all keys and corresponding values of either an
+ * array or iterable object.
+ *
+ * @param iterable $values
+ *
+ * @psalm-param iterable $values
+ */
+ public function putAll(iterable $values)
+ {
+ foreach ($values as $key => $value) {
+ $this->put($key, $value);
+ }
+ }
+
+ /**
+ * Iteratively reduces the map to a single value using a callback.
+ *
+ * @param callable $callback Accepts the carry, key, and value, and
+ * returns an updated carry value.
+ *
+ * @param mixed|null $initial Optional initial carry value.
+ *
+ * @return mixed The carry value of the final iteration, or the initial
+ * value if the map was empty.
+ *
+ * @template TCarry
+ * @psalm-param callable(TCarry, TKey, TValue): TCarry $callback
+ * @psalm-param TCarry $initial
+ * @psalm-return TCarry
+ */
+ public function reduce(callable $callback, $initial = null)
+ {
+ $carry = $initial;
+
+ foreach ($this->pairs as $pair) {
+ $carry = $callback($carry, $pair->key, $pair->value);
+ }
+
+ return $carry;
+ }
+
+ /**
+ * Completely removes a pair from the internal array by position. It is
+ * important to remove it from the array and not just use 'unset'.
+ *
+ * @return mixed
+ *
+ * @psalm-return TValue
+ */
+ private function delete(int $position)
+ {
+ $pair = $this->pairs[$position];
+ $value = $pair->value;
+
+ array_splice($this->pairs, $position, 1, null);
+ $this->checkCapacity();
+
+ return $value;
+ }
+
+ /**
+ * Removes a key's association from the map and returns the associated value
+ * or a provided default if provided.
+ *
+ * @param mixed $key
+ * @param mixed $default
+ *
+ * @return mixed The associated value or fallback default if provided.
+ *
+ * @throws \OutOfBoundsException if no default was provided and the key is
+ * not associated with a value.
+ *
+ * @template TDefault
+ * @psalm-param TKey $key
+ * @psalm-param TDefault $default
+ * @psalm-return TValue|TDefault
+ */
+ public function remove($key, $default = null)
+ {
+ foreach ($this->pairs as $position => $pair) {
+ if ($this->keysAreEqual($pair->key, $key)) {
+ return $this->delete($position);
+ }
+ }
+
+ // Check if a default was provided
+ if (func_num_args() === 1) {
+ throw new \OutOfBoundsException();
+ }
+
+ return $default;
+ }
+
+ /**
+ * Reverses the map in-place
+ */
+ public function reverse()
+ {
+ $this->pairs = array_reverse($this->pairs);
+ }
+
+ /**
+ * Returns a reversed copy of the map.
+ *
+ * @return Map
+ *
+ * @psalm-return Map
+ */
+ public function reversed(): Map
+ {
+ $reversed = new self();
+ $reversed->pairs = array_reverse($this->pairs);
+
+ return $reversed;
+ }
+
+ /**
+ * Returns a sub-sequence of a given length starting at a specified offset.
+ *
+ * @param int $offset If the offset is non-negative, the map will
+ * start at that offset in the map. If offset is
+ * negative, the map will start that far from the
+ * end.
+ *
+ * @param int|null $length If a length is given and is positive, the
+ * resulting set will have up to that many pairs in
+ * it. If the requested length results in an
+ * overflow, only pairs up to the end of the map
+ * will be included.
+ *
+ * If a length is given and is negative, the map
+ * will stop that many pairs from the end.
+ *
+ * If a length is not provided, the resulting map
+ * will contains all pairs between the offset and
+ * the end of the map.
+ *
+ * @return Map
+ *
+ * @psalm-return Map
+ */
+ public function slice(int $offset, int|null $length = null): Map
+ {
+ $map = new self();
+
+ if (func_num_args() === 1) {
+ $slice = array_slice($this->pairs, $offset);
+ } else {
+ $slice = array_slice($this->pairs, $offset, $length);
+ }
+
+ foreach ($slice as $pair) {
+ $map->put($pair->key, $pair->value);
+ }
+
+ return $map;
+ }
+
+ /**
+ * Sorts the map in-place, based on an optional callable comparator.
+ *
+ * The map will be sorted by value.
+ *
+ * @param callable|null $comparator Accepts two values to be compared.
+ *
+ * @psalm-param (callable(TValue, TValue): int)|null $comparator
+ */
+ public function sort(callable|null $comparator = null)
+ {
+ if ($comparator) {
+ usort($this->pairs, function ($a, $b) use ($comparator) {
+ return $comparator($a->value, $b->value);
+ });
+
+ } else {
+ usort($this->pairs, function ($a, $b) {
+ return $a->value <=> $b->value;
+ });
+ }
+ }
+
+ /**
+ * Returns a sorted copy of the map, based on an optional callable
+ * comparator. The map will be sorted by value.
+ *
+ * @param callable|null $comparator Accepts two values to be compared.
+ *
+ * @return Map
+ *
+ * @psalm-param (callable(TValue, TValue): int)|null $comparator
+ * @psalm-return Map
+ */
+ public function sorted(callable|null $comparator = null): Map
+ {
+ $copy = $this->copy();
+ $copy->sort($comparator);
+ return $copy;
+ }
+
+ /**
+ * Sorts the map in-place, based on an optional callable comparator.
+ *
+ * The map will be sorted by key.
+ *
+ * @param callable|null $comparator Accepts two keys to be compared.
+ *
+ * @psalm-param (callable(TKey, TKey): int)|null $comparator
+ */
+ public function ksort(callable|null $comparator = null)
+ {
+ if ($comparator) {
+ usort($this->pairs, function ($a, $b) use ($comparator) {
+ return $comparator($a->key, $b->key);
+ });
+
+ } else {
+ usort($this->pairs, function ($a, $b) {
+ return $a->key <=> $b->key;
+ });
+ }
+ }
+
+ /**
+ * Returns a sorted copy of the map, based on an optional callable
+ * comparator. The map will be sorted by key.
+ *
+ * @param callable|null $comparator Accepts two keys to be compared.
+ *
+ * @return Map
+ *
+ * @psalm-param (callable(TKey, TKey): int)|null $comparator
+ * @psalm-return Map
+ */
+ public function ksorted(callable|null $comparator = null): Map
+ {
+ $copy = $this->copy();
+ $copy->ksort($comparator);
+ return $copy;
+ }
+
+ /**
+ * Returns the sum of all values in the map.
+ *
+ * @return int|float The sum of all the values in the map.
+ */
+ public function sum()
+ {
+ return $this->values()->sum();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toArray(): array
+ {
+ $array = [];
+
+ foreach ($this->pairs as $pair) {
+ $array[$pair->key] = $pair->value;
+ }
+
+ return $array;
+ }
+
+ /**
+ * Returns a sequence of all the associated values in the Map.
+ *
+ * @return Sequence
+ *
+ * @psalm-return Sequence
+ */
+ public function values(): Sequence
+ {
+ $value = function ($pair) {
+ return $pair->value;
+ };
+
+ return new Vector(array_map($value, $this->pairs));
+ }
+
+ /**
+ * Creates a new map that contains the pairs of the current instance as well
+ * as the pairs of another map.
+ *
+ * @param Map $map The other map, to combine with the current instance.
+ *
+ * @return Map A new map containing all the pairs of the current
+ * instance as well as another map.
+ *
+ * @template TKey2
+ * @template TValue2
+ * @psalm-param Map $map
+ * @psalm-return Map
+ */
+ public function union(Map $map): Map
+ {
+ return $this->merge($map);
+ }
+
+ /**
+ * Creates a new map using keys of either the current instance or of another
+ * map, but not of both.
+ *
+ * @param Map $map
+ *
+ * @return Map A new map containing keys in the current instance as well
+ * as another map, but not in both.
+ *
+ * @template TKey2
+ * @template TValue2
+ * @psalm-param Map $map
+ * @psalm-return Map
+ */
+ public function xor(Map $map): Map
+ {
+ return $this->merge($map)->filter(function ($key) use ($map) {
+ return $this->hasKey($key) ^ $map->hasKey($key);
+ });
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ foreach ($this->pairs as $pair) {
+ yield $pair->key => $pair->value;
+ }
+ }
+
+ /**
+ * Returns a representation to be used for var_dump and print_r.
+ *
+ * @psalm-return array>
+ */
+ public function __debugInfo()
+ {
+ return $this->pairs()->toArray();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ $this->put($offset, $value);
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws OutOfBoundsException
+ */
+ #[\ReturnTypeWillChange]
+ public function &offsetGet($offset)
+ {
+ $pair = $this->lookupKey($offset);
+
+ if ($pair) {
+ return $pair->value;
+ }
+ throw new OutOfBoundsException();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ $this->remove($offset, null);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ return $this->get($offset, null) !== null;
+ }
+
+ /**
+ * Returns a representation that can be natively converted to JSON, which is
+ * called when invoking json_encode.
+ *
+ * @return mixed
+ *
+ * @see \JsonSerializable
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ return (object)$this->toArray();
+ }
+}
diff --git a/packages/php-ds/src/Pair.php b/packages/php-ds/src/Pair.php
new file mode 100644
index 0000000..e3a97b8
--- /dev/null
+++ b/packages/php-ds/src/Pair.php
@@ -0,0 +1,158 @@
+key = $key;
+ $this->value = $value;
+ }
+
+ /**
+ *
+ * @param mixed $name
+ *
+ * @return mixed|null
+ */
+ public function __isset($name)
+ {
+ if ($name === 'key' || $name === 'value') {
+ return $this->$name !== null;
+ }
+ return false;
+ }
+
+ /**
+ * This allows unset($pair->key) to not completely remove the property,
+ * but be set to null instead.
+ *
+ * @return void
+ */
+ public function __unset(string $name)
+ {
+ if ($name === 'key' || $name === 'value') {
+ $this->$name = null;
+ return;
+ }
+ throw new OutOfBoundsException();
+ }
+
+ /**
+ * @param mixed $name
+ *
+ * @return mixed|null
+ */
+ public function &__get($name)
+ {
+ if ($name === 'key' || $name === 'value') {
+ return $this->$name;
+ }
+ throw new OutOfBoundsException();
+ }
+
+ /**
+ * @param mixed $name
+ * @param mixed $value
+ *
+ * @return mixed|null
+ */
+ public function __set($name, $value)
+ {
+ if ($name === 'key' || $name === 'value') {
+ $this->$name = $value;
+ return;
+ }
+ throw new OutOfBoundsException();
+ }
+
+ /**
+ * Returns a copy of the Pair
+ *
+ * @psalm-return self
+ */
+ public function copy(): self
+ {
+ return new self($this->key, $this->value);
+ }
+
+ /**
+ * Returns a representation to be used for var_dump and print_r.
+ *
+ * @return array
+ *
+ * @psalm-return array{key: TKey, value: TValue}
+ */
+ public function __debugInfo()
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @psalm-return array{key: TKey, value: TValue}
+ */
+ public function toArray(): array
+ {
+ return [
+ 'key' => $this->key,
+ 'value' => $this->value,
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @psalm-return array{key: TKey, value: TValue}
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize()
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * Returns a string representation of the pair.
+ */
+ public function __toString()
+ {
+ return 'object(' . get_class($this) . ')';
+ }
+}
diff --git a/packages/php-ds/src/PriorityQueue.php b/packages/php-ds/src/PriorityQueue.php
new file mode 100644
index 0000000..5cd71d3
--- /dev/null
+++ b/packages/php-ds/src/PriorityQueue.php
@@ -0,0 +1,340 @@
+
+ */
+final class PriorityQueue implements Collection
+{
+ use Traits\GenericCollection;
+ use Traits\SquaredCapacity;
+
+ public const MIN_CAPACITY = 8;
+
+ /**
+ * @var array>
+ */
+ private $heap = [];
+
+ /**
+ * @var int
+ */
+ private $stamp = 0;
+
+ /**
+ * Creates a new instance.
+ */
+ public function __construct()
+ {
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clear()
+ {
+ $this->heap = [];
+ $this->stamp = 0;
+ $this->capacity = self::MIN_CAPACITY;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function copy(): self
+ {
+ $copy = new PriorityQueue();
+
+ $copy->heap = $this->heap;
+ $copy->stamp = $this->stamp;
+ $copy->capacity = $this->capacity;
+
+ return $copy;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function count(): int
+ {
+ return count($this->heap);
+ }
+
+ /**
+ * Returns the value with the highest priority in the priority queue.
+ *
+ * @return mixed
+ *
+ * @throw UnderflowException
+ *
+ * @psalm-return TValue
+ */
+ public function peek()
+ {
+ if ($this->isEmpty()) {
+ throw new UnderflowException();
+ }
+
+ return $this->heap[0]->value;
+ }
+
+ /**
+ * Returns the index of a node's left leaf.
+ *
+ * @param int $index The index of the node.
+ *
+ * @return int The index of the left leaf.
+ */
+ private function left(int $index): int
+ {
+ return ($index * 2) + 1;
+ }
+
+ /**
+ * Returns the index of a node's right leaf.
+ *
+ * @param int $index The index of the node.
+ *
+ * @return int The index of the right leaf.
+ */
+ private function right(int $index): int
+ {
+ return ($index * 2) + 2;
+ }
+
+ /**
+ * Returns the index of a node's parent node.
+ *
+ * @param int $index The index of the node.
+ *
+ * @return int The index of the parent.
+ */
+ private function parent(int $index): int
+ {
+ return (int) (($index - 1) / 2);
+ }
+
+ /**
+ * Compares two indices of the heap.
+ *
+ * @return int
+ */
+ private function compare(int $a, int $b)
+ {
+ $x = $this->heap[$a];
+ $y = $this->heap[$b];
+
+ // Compare priority, using insertion stamp as fallback.
+ return ($x->priority <=> $y->priority) ?: ($y->stamp <=> $x->stamp);
+ }
+
+ /**
+ * Swaps the nodes at two indices of the heap.
+ */
+ private function swap(int $a, int $b)
+ {
+ $temp = $this->heap[$a];
+ $this->heap[$a] = $this->heap[$b];
+ $this->heap[$b] = $temp;
+ }
+
+ /**
+ * Returns the index of a node's largest leaf node.
+ *
+ * @param int $parent the parent node.
+ *
+ * @return int the index of the node's largest leaf node.
+ */
+ private function getLargestLeaf(int $parent)
+ {
+ $left = $this->left($parent);
+ $right = $this->right($parent);
+
+ if ($right < count($this->heap) && $this->compare($left, $right) < 0) {
+ return $right;
+ }
+
+ return $left;
+ }
+
+ /**
+ * Starts the process of sifting down a given node index to ensure that
+ * the heap's properties are preserved.
+ */
+ private function siftDown(int $node)
+ {
+ $last = floor(count($this->heap) / 2);
+
+ for ($parent = $node; $parent < $last; $parent = $leaf) {
+
+ // Determine the largest leaf to potentially swap with the parent.
+ $leaf = $this->getLargestLeaf($parent);
+
+ // Done if the parent is not greater than its largest leaf
+ if ($this->compare($parent, $leaf) > 0) {
+ break;
+ }
+
+ $this->swap($parent, $leaf);
+ }
+ }
+
+ /**
+ * Sets the root node and sifts it down the heap.
+ *
+ * @param PriorityNode $node
+ */
+ private function setRoot(PriorityNode $node)
+ {
+ $this->heap[0] = $node;
+ $this->siftDown(0);
+ }
+
+ /**
+ * Returns the root node of the heap.
+ *
+ * @return PriorityNode
+ */
+ private function getRoot(): PriorityNode
+ {
+ return $this->heap[0];
+ }
+
+ /**
+ * Returns and removes the value with the highest priority in the queue.
+ *
+ * @return mixed
+ *
+ * @psalm-return TValue
+ */
+ public function pop()
+ {
+ if ($this->isEmpty()) {
+ throw new UnderflowException();
+ }
+
+ // Last leaf of the heap to become the new root.
+ $leaf = array_pop($this->heap);
+
+ if (empty($this->heap)) {
+ return $leaf->value;
+ }
+
+ // Cache the current root value to return before replacing with next.
+ $value = $this->getRoot()->value;
+
+ // Replace the root, then sift down.
+ $this->setRoot($leaf);
+ $this->checkCapacity();
+
+ return $value;
+ }
+
+ /**
+ * Sifts a node up the heap until it's in the right position.
+ */
+ private function siftUp(int $leaf)
+ {
+ for (; $leaf > 0; $leaf = $parent) {
+ $parent = $this->parent($leaf);
+
+ // Done when parent priority is greater.
+ if ($this->compare($leaf, $parent) < 0) {
+ break;
+ }
+
+ $this->swap($parent, $leaf);
+ }
+ }
+
+ /**
+ * Pushes a value into the queue, with a specified priority.
+ *
+ * @param mixed $value
+ *
+ * @psalm-param TValue $value
+ */
+ public function push($value, int $priority)
+ {
+ $this->checkCapacity();
+
+ // Add new leaf, then sift up to maintain heap,
+ $this->heap[] = new PriorityNode($value, $priority, $this->stamp++);
+ $this->siftUp(count($this->heap) - 1);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toArray(): array
+ {
+ $heap = $this->heap;
+ $array = [];
+
+ while ( ! $this->isEmpty()) {
+ $array[] = $this->pop();
+ }
+
+ $this->heap = $heap;
+ return $array;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ while ( ! $this->isEmpty()) {
+ yield $this->pop();
+ }
+ }
+}
+
+/**
+ * @internal
+ *
+ * @template TValue
+ */
+final class PriorityNode
+{
+ /**
+ * @var mixed
+ *
+ * @psalm-var TValue
+ */
+ public $value;
+
+ /**
+ * @var int
+ */
+ public $priority;
+
+ /**
+ * @var int
+ */
+ public $stamp;
+
+ /**
+ * @param mixed $value
+ * @param int $priority
+ * @param int $stamp
+ *
+ * @psalm-param TValue $value
+ */
+ public function __construct($value, int $priority, int $stamp)
+ {
+ $this->value = $value;
+ $this->priority = $priority;
+ $this->stamp = $stamp;
+ }
+}
diff --git a/packages/php-ds/src/Queue.php b/packages/php-ds/src/Queue.php
new file mode 100644
index 0000000..8628b25
--- /dev/null
+++ b/packages/php-ds/src/Queue.php
@@ -0,0 +1,197 @@
+
+ * @implements \ArrayAccess
+ * @template-use Traits\GenericCollection
+ */
+final class Queue implements Collection, \ArrayAccess
+{
+ use Traits\GenericCollection;
+
+ /**
+ * @var Deque internal deque to store values.
+ *
+ * @psalm-var Deque
+ */
+ private $deque;
+
+ /**
+ * Creates an instance using the values of an array or Traversable object.
+ *
+ * @param iterable $values
+ *
+ * @psalm-param iterable $values
+ */
+ public function __construct(iterable $values = [])
+ {
+ $this->deque = new Deque($values);
+ }
+
+ /**
+ * Ensures that enough memory is allocated for a specified capacity. This
+ * potentially reduces the number of reallocations as the size increases.
+ *
+ * @param int $capacity The number of values for which capacity should be
+ * allocated. Capacity will stay the same if this value
+ * is less than or equal to the current capacity.
+ */
+ public function allocate(int $capacity)
+ {
+ $this->deque->allocate($capacity);
+ }
+
+ /**
+ * Returns the current capacity of the queue.
+ */
+ public function capacity(): int
+ {
+ return $this->deque->capacity();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function clear()
+ {
+ $this->deque->clear();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function copy(): self
+ {
+ return new self($this->deque);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function count(): int
+ {
+ return count($this->deque);
+ }
+
+ /**
+ * Returns the value at the front of the queue without removing it.
+ *
+ * @return mixed
+ *
+ * @psalm-return TValue
+ */
+ public function peek()
+ {
+ return $this->deque->first();
+ }
+
+ /**
+ * Returns and removes the value at the front of the Queue.
+ *
+ * @return mixed
+ *
+ * @psalm-return TValue
+ */
+ public function pop()
+ {
+ return $this->deque->shift();
+ }
+
+ /**
+ * Pushes zero or more values into the back of the queue.
+ *
+ * @param mixed ...$values
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function push(...$values)
+ {
+ $this->deque->push(...$values);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toArray(): array
+ {
+ return $this->deque->toArray();
+ }
+
+ /**
+ * Get iterator
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ while ( ! $this->isEmpty()) {
+ yield $this->pop();
+ }
+ }
+
+
+ /**
+ * @inheritdoc
+ *
+ * @throws OutOfBoundsException
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ if ($offset === null) {
+ $this->push($value);
+ } else {
+ throw new Error();
+ }
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws Error
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset)
+ {
+ throw new Error();
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws Error
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ throw new Error();
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws Error
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ throw new Error();
+ }
+
+ /**
+ * Ensures that the internal sequence will be cloned too.
+ */
+ public function __clone()
+ {
+ $this->deque = clone $this->deque;
+ }
+}
diff --git a/packages/php-ds/src/Sequence.php b/packages/php-ds/src/Sequence.php
new file mode 100644
index 0000000..4f170a3
--- /dev/null
+++ b/packages/php-ds/src/Sequence.php
@@ -0,0 +1,331 @@
+
+ * @extends \ArrayAccess
+ */
+interface Sequence extends Collection, \ArrayAccess
+{
+ /**
+ * Ensures that enough memory is allocated for a required capacity.
+ *
+ * @param int $capacity The number of values for which capacity should be
+ * allocated. Capacity will stay the same if this value
+ * is less than or equal to the current capacity.
+ */
+ public function allocate(int $capacity);
+
+ /**
+ * Updates every value in the sequence by applying a callback, using the
+ * return value as the new value.
+ *
+ * @param callable $callback Accepts the value, returns the new value.
+ *
+ * @psalm-param callable(TValue): TValue $callback
+ */
+ public function apply(callable $callback);
+
+ /**
+ * Returns the current capacity of the sequence.
+ *
+ * @return int
+ */
+ public function capacity(): int;
+
+ /**
+ * Determines whether the sequence contains all of zero or more values.
+ *
+ * @param mixed ...$values
+ *
+ * @return bool true if at least one value was provided and the sequence
+ * contains all given values, false otherwise.
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function contains(...$values): bool;
+
+ /**
+ * Returns a new sequence containing only the values for which a callback
+ * returns true. A boolean test will be used if a callback is not provided.
+ *
+ * @param callable|null $callback Accepts a value, returns a boolean result:
+ * true : include the value,
+ * false: skip the value.
+ *
+ * @return Sequence
+ *
+ * @psalm-param (callable(TValue): bool)|null $callback
+ * @psalm-return Sequence
+ */
+ public function filter(callable|null $callback = null): Sequence;
+
+ /**
+ * Returns the index of a given value, or null if it could not be found.
+ *
+ * @param mixed $value
+ *
+ * @return int|null
+ *
+ * @psalm-param TValue $value
+ */
+ public function find($value);
+
+ /**
+ * Returns the first value in the sequence.
+ *
+ * @return mixed
+ *
+ * @throws \UnderflowException if the sequence is empty.
+ *
+ * @psalm-return TValue
+ */
+ public function first();
+
+ /**
+ * Returns the value at a given index (position) in the sequence.
+ *
+ * @return mixed
+ *
+ * @throws \OutOfRangeException if the index is not in the range [0, size-1]
+ *
+ * @psalm-return TValue
+ */
+ public function get(int $index);
+
+ /**
+ * Inserts zero or more values at a given index.
+ *
+ * Each value after the index will be moved one position to the right.
+ * Values may be inserted at an index equal to the size of the sequence.
+ *
+ * @param mixed ...$values
+ *
+ * @throws \OutOfRangeException if the index is not in the range [0, n]
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function insert(int $index, ...$values);
+
+ /**
+ * Joins all values of the sequence into a string, adding an optional 'glue'
+ * between them. Returns an empty string if the sequence is empty.
+ */
+ public function join(string $glue = null): string;
+
+ /**
+ * Returns the last value in the sequence.
+ *
+ * @return mixed
+ *
+ * @throws \UnderflowException if the sequence is empty.
+ *
+ * @psalm-return TValue
+ */
+ public function last();
+
+ /**
+ * Returns a new sequence using the results of applying a callback to each
+ * value.
+ *
+ * @param callable $callback
+ *
+ * @return Sequence
+ *
+ * @template TNewValue
+ * @psalm-param callable(TValue): TNewValue $callback
+ * @psalm-return Sequence
+ */
+ public function map(callable $callback): Sequence;
+
+ /**
+ * Returns the result of adding all given values to the sequence.
+ *
+ * @param array|\Traversable $values
+ *
+ * @return Sequence
+ *
+ * @template TValue2
+ * @psalm-param iterable $values
+ * @psalm-return Sequence
+ */
+ public function merge($values): Sequence;
+
+ /**
+ * Removes the last value in the sequence, and returns it.
+ *
+ * @return mixed what was the last value in the sequence.
+ *
+ * @throws \UnderflowException if the sequence is empty.
+ *
+ * @psalm-return TValue
+ */
+ public function pop();
+
+ /**
+ * Adds zero or more values to the end of the sequence.
+ *
+ * @param mixed ...$values
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function push(...$values);
+
+ /**
+ * Iteratively reduces the sequence to a single value using a callback.
+ *
+ * @param callable $callback Accepts the carry and current value, and
+ * returns an updated carry value.
+ *
+ * @param mixed|null $initial Optional initial carry value.
+ *
+ * @return mixed The carry value of the final iteration, or the initial
+ * value if the sequence was empty.
+ *
+ * @template TCarry
+ * @psalm-param callable(TCarry, TValue): TCarry $callback
+ * @psalm-param TCarry $initial
+ * @psalm-return TCarry
+ */
+ public function reduce(callable $callback, $initial = null);
+
+ /**
+ * Removes and returns the value at a given index in the sequence.
+ *
+ * @param int $index this index to remove.
+ *
+ * @return mixed the removed value.
+ *
+ * @throws \OutOfRangeException if the index is not in the range [0, size-1]
+ *
+ * @psalm-return TValue
+ */
+ public function remove(int $index);
+
+ /**
+ * Reverses the sequence in-place.
+ */
+ public function reverse();
+
+ /**
+ * Returns a reversed copy of the sequence.
+ *
+ * @return Sequence
+ *
+ * @psalm-return Sequence
+ */
+ public function reversed();
+
+ /**
+ * Rotates the sequence by a given number of rotations, which is equivalent
+ * to successive calls to 'shift' and 'push' if the number of rotations is
+ * positive, or 'pop' and 'unshift' if negative.
+ *
+ * @param int $rotations The number of rotations (can be negative).
+ */
+ public function rotate(int $rotations);
+
+ /**
+ * Replaces the value at a given index in the sequence with a new value.
+ *
+ * @param mixed $value
+ *
+ * @throws \OutOfRangeException if the index is not in the range [0, size-1]
+ *
+ * @psalm-param TValue $value
+ */
+ public function set(int $index, $value);
+
+ /**
+ * Removes and returns the first value in the sequence.
+ *
+ * @return mixed what was the first value in the sequence.
+ *
+ * @throws \UnderflowException if the sequence was empty.
+ *
+ * @psalm-return TValue
+ */
+ public function shift();
+
+ /**
+ * Returns a sub-sequence of a given length starting at a specified index.
+ *
+ * @param int $index If the index is positive, the sequence will start
+ * at that index in the sequence. If index is negative,
+ * the sequence will start that far from the end.
+ *
+ * @param int $length If a length is given and is positive, the resulting
+ * sequence will have up to that many values in it.
+ * If the length results in an overflow, only values
+ * up to the end of the sequence will be included.
+ *
+ * If a length is given and is negative, the sequence
+ * will stop that many values from the end.
+ *
+ * If a length is not provided, the resulting sequence
+ * will contain all values between the index and the
+ * end of the sequence.
+ *
+ * @return Sequence
+ *
+ * @psalm-return Sequence
+ */
+ public function slice(int $index, int $length = null): Sequence;
+
+ /**
+ * Sorts the sequence in-place, based on an optional callable comparator.
+ *
+ * @param callable|null $comparator Accepts two values to be compared.
+ * Should return the result of a <=> b.
+ *
+ * @psalm-param (callable(TValue, TValue): int)|null $comparator
+ */
+ public function sort(callable $comparator = null);
+
+ /**
+ * Returns a sorted copy of the sequence, based on an optional callable
+ * comparator. Natural ordering will be used if a comparator is not given.
+ *
+ * @param callable|null $comparator Accepts two values to be compared.
+ * Should return the result of a <=> b.
+ *
+ * @return Sequence
+ *
+ * @psalm-param (callable(TValue, TValue): int)|null $comparator
+ * @psalm-return Sequence
+ */
+ public function sorted(callable $comparator = null): Sequence;
+
+ /**
+ * Returns the sum of all values in the sequence.
+ *
+ * @return int|float The sum of all the values in the sequence.
+ */
+ public function sum();
+
+ /**
+ * @inheritDoc
+ *
+ * @return list
+ */
+ function toArray(): array;
+
+ /**
+ * Adds zero or more values to the front of the sequence.
+ *
+ * @param mixed ...$values
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function unshift(...$values);
+}
diff --git a/packages/php-ds/src/Set.php b/packages/php-ds/src/Set.php
new file mode 100644
index 0000000..2eedaca
--- /dev/null
+++ b/packages/php-ds/src/Set.php
@@ -0,0 +1,541 @@
+
+ * @implements \ArrayAccess
+ * @template-use Traits\GenericCollection
+ */
+final class Set implements Collection, \ArrayAccess
+{
+ use Traits\GenericCollection;
+
+ public const MIN_CAPACITY = Map::MIN_CAPACITY;
+
+ /**
+ * @var Map internal map to store the values.
+ *
+ * @psalm-var Map
+ */
+ private $table;
+
+ /**
+ * Creates a new set using the values of an array or Traversable object.
+ * The keys of either will not be preserved.
+ *
+ * @param iterable $values
+ *
+ * @psalm-param iterable $values
+ */
+ public function __construct(iterable $values = [])
+ {
+ $this->table = new Map();
+
+ foreach ($values as $value) {
+ $this->add($value);
+ }
+ }
+
+ /**
+ * Adds zero or more values to the set.
+ *
+ * @param mixed ...$values
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function add(...$values)
+ {
+ foreach ($values as $value) {
+ $this->table->put($value, null);
+ }
+ }
+
+ /**
+ * Ensures that enough memory is allocated for a specified capacity. This
+ * potentially reduces the number of reallocations as the size increases.
+ *
+ * @param int $capacity The number of values for which capacity should be
+ * allocated. Capacity will stay the same if this value
+ * is less than or equal to the current capacity.
+ */
+ public function allocate(int $capacity)
+ {
+ $this->table->allocate($capacity);
+ }
+
+ /**
+ * Returns the current capacity of the set.
+ */
+ public function capacity(): int
+ {
+ return $this->table->capacity();
+ }
+
+ /**
+ * Clear all elements in the Set
+ */
+ public function clear()
+ {
+ $this->table->clear();
+ }
+
+ /**
+ * Determines whether the set contains all of zero or more values.
+ *
+ * @param mixed ...$values
+ *
+ * @return bool true if at least one value was provided and the set
+ * contains all given values, false otherwise.
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function contains(...$values): bool
+ {
+ foreach ($values as $value) {
+ if ( ! $this->table->hasKey($value)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function copy(): self
+ {
+ return new self($this);
+ }
+
+ /**
+ * Returns the number of elements in the Stack
+ *
+ * @return int
+ */
+ public function count(): int
+ {
+ return count($this->table);
+ }
+
+ /**
+ * Creates a new set using values from this set that aren't in another set.
+ *
+ * Formally: A \ B = {x ∈ A | x ∉ B}
+ *
+ * @param Set $set
+ *
+ * @return Set
+ *
+ * @template TValue2
+ * @psalm-param Set $set
+ * @psalm-return Set
+ */
+ public function diff(Set $set): Set
+ {
+ return $this->table->diff($set->table)->keys();
+ }
+
+ /**
+ * Creates a new set using values in either this set or in another set,
+ * but not in both.
+ *
+ * Formally: A ⊖ B = {x : x ∈ (A \ B) ∪ (B \ A)}
+ *
+ * @param Set $set
+ *
+ * @return Set
+ *
+ * @template TValue2
+ * @psalm-param Set $set
+ * @psalm-return Set
+ */
+ public function xor(Set $set): Set
+ {
+ return $this->table->xor($set->table)->keys();
+ }
+
+ /**
+ * Returns a new set containing only the values for which a callback
+ * returns true. A boolean test will be used if a callback is not provided.
+ *
+ * @param callable|null $callback Accepts a value, returns a boolean:
+ * true : include the value,
+ * false: skip the value.
+ *
+ * @return Set
+ *
+ * @psalm-param (callable(TValue): bool)|null $callback
+ * @psalm-return Set
+ */
+ public function filter(callable|null $callback = null): Set
+ {
+ return new self(array_filter($this->toArray(), $callback ?: 'boolval'));
+ }
+
+ /**
+ * Returns the first value in the set.
+ *
+ * @return mixed the first value in the set.
+ *
+ * @psalm-return TValue
+ */
+ public function first()
+ {
+ return $this->table->first()->key;
+ }
+
+ /**
+ * Returns the value at a specified position in the set.
+ *
+ * @return mixed|null
+ *
+ * @throws OutOfRangeException
+ *
+ * @psalm-return TValue
+ */
+ public function get(int $position)
+ {
+ return $this->table->skip($position)->key;
+ }
+
+ /**
+ * Creates a new set using values common to both this set and another set.
+ *
+ * In other words, returns a copy of this set with all values removed that
+ * aren't in the other set.
+ *
+ * Formally: A ∩ B = {x : x ∈ A ∧ x ∈ B}
+ *
+ * @param Set $set
+ *
+ * @return Set
+ *
+ * @template TValue2
+ * @psalm-param Set $set
+ * @psalm-return Set
+ */
+ public function intersect(Set $set): Set
+ {
+ return $this->table->intersect($set->table)->keys();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isEmpty(): bool
+ {
+ return $this->table->isEmpty();
+ }
+
+ /**
+ * Joins all values of the set into a string, adding an optional 'glue'
+ * between them. Returns an empty string if the set is empty.
+ *
+ * @param string|null $glue
+ * @return string
+ */
+ public function join(string|null $glue = null): string
+ {
+ return implode($glue ?? '', $this->toArray());
+ }
+
+ /**
+ * Returns the last value in the set.
+ *
+ * @return mixed the last value in the set.
+ *
+ * @psalm-return TValue
+ */
+ public function last()
+ {
+ return $this->table->last()->key;
+ }
+
+ /**
+ * Returns a new set using the results of applying a callback to each
+ * value.
+ *
+ * @param callable $callback
+ *
+ * @return Set
+ *
+ * @template TNewValue
+ * @psalm-param callable(TValue): TNewValue $callback
+ * @psalm-return Set
+ */
+ public function map(callable $callback) {
+ return new self(array_map($callback, $this->toArray()));
+ }
+
+ /**
+ * Iteratively reduces the set to a single value using a callback.
+ *
+ * @param callable $callback Accepts the carry and current value, and
+ * returns an updated carry value.
+ *
+ * @param mixed|null $initial Optional initial carry value.
+ *
+ * @return mixed The carry value of the final iteration, or the initial
+ * value if the set was empty.
+ *
+ * @template TCarry
+ * @psalm-param callable(TCarry, TValue): TCarry $callback
+ * @psalm-param TCarry $initial
+ * @psalm-return TCarry
+ */
+ public function reduce(callable $callback, $initial = null)
+ {
+ $carry = $initial;
+
+ foreach ($this as $value) {
+ $carry = $callback($carry, $value);
+ }
+
+ return $carry;
+ }
+
+ /**
+ * Removes zero or more values from the set.
+ *
+ * @param mixed ...$values
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function remove(...$values)
+ {
+ foreach ($values as $value) {
+ $this->table->remove($value, null);
+ }
+ }
+
+ /**
+ * Reverses the set in-place.
+ */
+ public function reverse()
+ {
+ $this->table->reverse();
+ }
+
+ /**
+ * Returns a reversed copy of the set.
+ *
+ * @return Set
+ *
+ * @psalm-return Set
+ */
+ public function reversed(): Set
+ {
+ $reversed = $this->copy();
+ $reversed->table->reverse();
+
+ return $reversed;
+ }
+
+ /**
+ * Returns a subset of a given length starting at a specified offset.
+ *
+ * @param int $offset If the offset is non-negative, the set will start
+ * at that offset in the set. If offset is negative,
+ * the set will start that far from the end.
+ *
+ * @param int $length If a length is given and is positive, the resulting
+ * set will have up to that many values in it.
+ * If the requested length results in an overflow, only
+ * values up to the end of the set will be included.
+ *
+ * If a length is given and is negative, the set
+ * will stop that many values from the end.
+ *
+ * If a length is not provided, the resulting set
+ * will contains all values between the offset and the
+ * end of the set.
+ *
+ * @return Set
+ *
+ * @psalm-return Set
+ */
+ public function slice(int $offset, int|null $length = null): Set
+ {
+ $sliced = new self();
+ $sliced->table = $this->table->slice($offset, $length);
+
+ return $sliced;
+ }
+
+ /**
+ * Sorts the set in-place, based on an optional callable comparator.
+ *
+ * @param callable|null $comparator Accepts two values to be compared.
+ * Should return the result of a <=> b.
+ *
+ * @psalm-param (callable(TValue, TValue): int)|null $comparator
+ */
+ public function sort(callable|null $comparator = null)
+ {
+ $this->table->ksort($comparator);
+ }
+
+ /**
+ * Returns a sorted copy of the set, based on an optional callable
+ * comparator. Natural ordering will be used if a comparator is not given.
+ *
+ * @param callable|null $comparator Accepts two values to be compared.
+ * Should return the result of a <=> b.
+ *
+ * @return Set
+ *
+ * @psalm-param (callable(TValue, TValue): int)|null $comparator
+ * @psalm-return Set
+ */
+ public function sorted(callable|null $comparator = null): Set
+ {
+ $sorted = $this->copy();
+ $sorted->table->ksort($comparator);
+
+ return $sorted;
+ }
+
+ /**
+ * Returns the result of adding all given values to the set.
+ *
+ * @param array|\Traversable $values
+ *
+ * @return Set
+ *
+ * @template TValue2
+ * @psalm-param iterable $values
+ * @psalm-return Set
+ */
+ public function merge($values): Set
+ {
+ $merged = $this->copy();
+
+ foreach ($values as $value) {
+ $merged->add($value);
+ }
+
+ return $merged;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toArray(): array
+ {
+ return iterator_to_array($this);
+ }
+
+ /**
+ * Returns the sum of all values in the set.
+ *
+ * @return int|float The sum of all the values in the set.
+ */
+ public function sum()
+ {
+ return array_sum($this->toArray());
+ }
+
+ /**
+ * Creates a new set that contains the values of this set as well as the
+ * values of another set.
+ *
+ * Formally: A ∪ B = {x: x ∈ A ∨ x ∈ B}
+ *
+ * @param Set $set
+ *
+ * @return Set
+ *
+ * @template TValue2
+ * @psalm-param Set $set
+ * @psalm-return Set
+ */
+ public function union(Set $set): Set
+ {
+ $union = new self();
+
+ foreach ($this as $value) {
+ $union->add($value);
+ }
+
+ foreach ($set as $value) {
+ $union->add($value);
+ }
+
+ return $union;
+ }
+
+ /**
+ * Get iterator
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ foreach ($this->table as $key => $value) {
+ yield $key;
+ }
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws OutOfBoundsException
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ if ($offset === null) {
+ $this->add($value);
+ return;
+ }
+ throw new Error();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset)
+ {
+ return $this->table->skip($offset)->key;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws Error
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ throw new Error();
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws Error
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ throw new Error();
+ }
+
+ /**
+ * Ensures that the internal table will be cloned too.
+ */
+ public function __clone()
+ {
+ $this->table = clone $this->table;
+ }
+}
diff --git a/packages/php-ds/src/Stack.php b/packages/php-ds/src/Stack.php
new file mode 100644
index 0000000..26eeb4a
--- /dev/null
+++ b/packages/php-ds/src/Stack.php
@@ -0,0 +1,200 @@
+
+ * @implements \ArrayAccess
+ * @template-use Traits\GenericCollection
+ */
+final class Stack implements Collection, \ArrayAccess
+{
+ use Traits\GenericCollection;
+
+ /**
+ * @var Vector internal vector to store values of the stack.
+ *
+ * @psalm-var Vector
+ */
+ private $vector;
+
+ /**
+ * Creates an instance using the values of an array or Traversable object.
+ *
+ * @param iterable $values
+ *
+ * @psalm-param iterable $values
+ */
+ public function __construct(iterable $values = [])
+ {
+ $this->vector = new Vector($values);
+ }
+
+ /**
+ * Clear all elements in the Stack
+ */
+ public function clear()
+ {
+ $this->vector->clear();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function copy(): self
+ {
+ return new self($this->vector);
+ }
+
+ /**
+ * Returns the number of elements in the Stack
+ */
+ public function count(): int
+ {
+ return count($this->vector);
+ }
+
+ /**
+ * Ensures that enough memory is allocated for a specified capacity. This
+ * potentially reduces the number of reallocations as the size increases.
+ *
+ * @param int $capacity The number of values for which capacity should be
+ * allocated. Capacity will stay the same if this value
+ * is less than or equal to the current capacity.
+ */
+ public function allocate(int $capacity)
+ {
+ $this->vector->allocate($capacity);
+ }
+
+ /**
+ * Returns the current capacity of the stack.
+ */
+ public function capacity(): int
+ {
+ return $this->vector->capacity();
+ }
+
+ /**
+ * Returns the value at the top of the stack without removing it.
+ *
+ * @return mixed
+ *
+ * @throws \UnderflowException if the stack is empty.
+ *
+ * @psalm-return TValue
+ */
+ public function peek()
+ {
+ return $this->vector->last();
+ }
+
+ /**
+ * Returns and removes the value at the top of the stack.
+ *
+ * @return mixed
+ *
+ * @throws \UnderflowException if the stack is empty.
+ *
+ * @psalm-return TValue
+ */
+ public function pop()
+ {
+ return $this->vector->pop();
+ }
+
+ /**
+ * Pushes zero or more values onto the top of the stack.
+ *
+ * @param mixed ...$values
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function push(...$values)
+ {
+ $this->vector->push(...$values);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function toArray(): array
+ {
+ return array_reverse($this->vector->toArray());
+ }
+
+ /**
+ *
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ while ( ! $this->isEmpty()) {
+ yield $this->pop();
+ }
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws OutOfBoundsException
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ if ($offset === null) {
+ $this->push($value);
+ } else {
+ throw new Error();
+ }
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws Error
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset)
+ {
+ throw new Error();
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws Error
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ throw new Error();
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @throws Error
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ throw new Error();
+ }
+
+ /**
+ * Ensures that the internal vector will be cloned too.
+ */
+ public function __clone()
+ {
+ $this->vector = clone $this->vector;
+ }
+}
diff --git a/packages/php-ds/src/Traits/Capacity.php b/packages/php-ds/src/Traits/Capacity.php
new file mode 100644
index 0000000..f59834e
--- /dev/null
+++ b/packages/php-ds/src/Traits/Capacity.php
@@ -0,0 +1,130 @@
+capacity;
+ }
+
+ /**
+ * Ensures that enough memory is allocated for a specified capacity. This
+ * potentially reduces the number of reallocations as the size increases.
+ *
+ * @param int $capacity The number of values for which capacity should be
+ * allocated. Capacity will stay the same if this value
+ * is less than or equal to the current capacity.
+ */
+ public function allocate(int $capacity)
+ {
+ $this->capacity = max($capacity, $this->capacity);
+ }
+
+ /**
+ * @return float the structures growth factor.
+ */
+ protected function getGrowthFactor(): float
+ {
+ return 2;
+ }
+
+ /**
+ * @return float to multiply by when decreasing capacity.
+ */
+ protected function getDecayFactor(): float
+ {
+ return 0.5;
+ }
+
+ /**
+ * @return float the ratio between size and capacity when capacity should be
+ * decreased.
+ */
+ protected function getTruncateThreshold(): float
+ {
+ return 0.25;
+ }
+
+ /**
+ * Checks and adjusts capacity if required.
+ */
+ protected function checkCapacity()
+ {
+ if ($this->shouldIncreaseCapacity()) {
+ $this->increaseCapacity();
+ } else {
+ if ($this->shouldDecreaseCapacity()) {
+ $this->decreaseCapacity();
+ }
+ }
+ }
+
+ /**
+ * @param int $total
+ */
+ protected function ensureCapacity(int $total)
+ {
+ if ($total > $this->capacity()) {
+ $this->capacity = max($total, $this->nextCapacity());
+ }
+ }
+
+ /**
+ * @return bool whether capacity should be increased.
+ */
+ protected function shouldIncreaseCapacity(): bool
+ {
+ return $this->count() >= $this->capacity();
+ }
+
+ protected function nextCapacity(): int
+ {
+ return (int) ($this->capacity() * $this->getGrowthFactor());
+ }
+
+ /**
+ * Called when capacity should be increased to accommodate new values.
+ */
+ protected function increaseCapacity()
+ {
+ $this->capacity = max(
+ $this->count(),
+ $this->nextCapacity()
+ );
+ }
+
+ /**
+ * Called when capacity should be decrease if it drops below a threshold.
+ */
+ protected function decreaseCapacity()
+ {
+ $this->capacity = max(
+ self::MIN_CAPACITY,
+ (int) ($this->capacity() * $this->getDecayFactor())
+ );
+ }
+
+ /**
+ * @return bool whether capacity should be increased.
+ */
+ protected function shouldDecreaseCapacity(): bool
+ {
+ return count($this) <= $this->capacity() * $this->getTruncateThreshold();
+ }
+}
diff --git a/packages/php-ds/src/Traits/GenericCollection.php b/packages/php-ds/src/Traits/GenericCollection.php
new file mode 100644
index 0000000..466946f
--- /dev/null
+++ b/packages/php-ds/src/Traits/GenericCollection.php
@@ -0,0 +1,81 @@
+toArray();
+ }
+
+ /**
+ * Creates a shallow copy of the collection.
+ *
+ * @return static a shallow copy of the collection.
+ *
+ * @psalm-return static
+ */
+ public function copy(): self
+ {
+ return new static($this);
+ }
+
+ /**
+ * Returns an array representation of the collection.
+ *
+ * The format of the returned array is implementation-dependent. Some
+ * implementations may throw an exception if an array representation
+ * could not be created (for example when object are used as keys).
+ *
+ * @return array
+ *
+ * @psalm-return array
+ */
+ abstract public function toArray(): array;
+
+ /**
+ * Invoked when calling var_dump.
+ *
+ * @return array
+ */
+ public function __debugInfo()
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * Returns a string representation of the collection, which is invoked when
+ * the collection is converted to a string.
+ */
+ public function __toString()
+ {
+ return 'object(' . get_class($this) . ')';
+ }
+}
diff --git a/packages/php-ds/src/Traits/GenericSequence.php b/packages/php-ds/src/Traits/GenericSequence.php
new file mode 100644
index 0000000..5ba17e9
--- /dev/null
+++ b/packages/php-ds/src/Traits/GenericSequence.php
@@ -0,0 +1,447 @@
+
+ */
+ private $array = [];
+
+ /**
+ * @param iterable $values
+ *
+ * @psalm-param iterable $values
+ */
+ public function __construct(iterable $values = [])
+ {
+ foreach ($values as $value) {
+ $this->push($value);
+ }
+
+ $this->capacity = max(
+ $values === null ? 0 : count($values),
+ $this::MIN_CAPACITY
+ );
+ }
+
+ /**
+ * @return list
+ */
+ public function toArray(): array
+ {
+ return $this->array;
+ }
+
+ /**
+ * @psalm-param callable(TValue): TValue $callback
+ */
+ public function apply(callable $callback)
+ {
+ foreach ($this->array as &$value) {
+ $value = $callback($value);
+ }
+ }
+
+ /**
+ * @template TValue2
+ * @psalm-param iterable $values
+ * @psalm-return Sequence
+ */
+ public function merge($values): Sequence
+ {
+ $copy = $this->copy();
+ $copy->push(...$values);
+ return $copy;
+ }
+
+ /**
+ *
+ */
+ public function count(): int
+ {
+ return count($this->array);
+ }
+
+ /**
+ * @psalm-param TValue ...$values
+ */
+ public function contains(...$values): bool
+ {
+ foreach ($values as $value) {
+ if ($this->find($value) === null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @psalm-param (callable(TValue): bool)|null $callback
+ * @psalm-return Sequence
+ */
+ public function filter(callable|null $callback = null): Sequence
+ {
+ return new self(array_filter($this->array, $callback ?: 'boolval'));
+ }
+
+ /**
+ * @return int|null
+ *
+ * @psalm-param TValue $value
+ */
+ public function find($value)
+ {
+ $offset = array_search($value, $this->array, true);
+
+ return $offset === false ? null : $offset;
+ }
+
+ /**
+ * @throws \UnderflowException if the sequence is empty.
+ *
+ * @psalm-return TValue
+ */
+ public function first()
+ {
+ if ($this->isEmpty()) {
+ throw new UnderflowException();
+ }
+
+ return $this->array[0];
+ }
+
+ /**
+ * @throws \OutOfRangeException if the index is not in the range [0, size-1]
+ *
+ * @psalm-return TValue
+ */
+ public function get(int $index)
+ {
+ if ( ! $this->validIndex($index)) {
+ throw new OutOfRangeException();
+ }
+
+ return $this->array[$index];
+ }
+
+ /**
+ * @throws \OutOfRangeException if the index is not in the range [0, n]
+ *
+ * @psalm-param TValue ...$values
+ */
+ public function insert(int $index, ...$values)
+ {
+ if ( ! $this->validIndex($index) && $index !== count($this)) {
+ throw new OutOfRangeException();
+ }
+
+ array_splice($this->array, $index, 0, $values);
+ $this->checkCapacity();
+ }
+
+ /**
+ *
+ */
+ public function join(string $glue = null): string
+ {
+ return implode($glue ?? '', $this->array);
+ }
+
+ /**
+ * @throws \UnderflowException if the sequence is empty.
+ *
+ * @psalm-return TValue
+ */
+ public function last()
+ {
+ if ($this->isEmpty()) {
+ throw new UnderflowException();
+ }
+
+ return $this->array[count($this) - 1];
+ }
+
+ /**
+ * @template TNewValue
+ * @psalm-param callable(TValue): TNewValue $callback
+ * @psalm-return Sequence
+ */
+ public function map(callable $callback): Sequence
+ {
+ return new self(array_map($callback, $this->array));
+ }
+
+ /**
+ * @throws \UnderflowException if the sequence is empty.
+ * @psalm-return TValue
+ */
+ public function pop()
+ {
+ if ($this->isEmpty()) {
+ throw new UnderflowException();
+ }
+
+ $value = array_pop($this->array);
+ $this->checkCapacity();
+
+ return $value;
+ }
+
+ /**
+ * @psalm-param TValue ...$values
+ */
+ public function push(...$values)
+ {
+ $this->ensureCapacity($this->count() + count($values));
+
+ foreach ($values as $value) {
+ $this->array[] = $value;
+ }
+ }
+
+ /**
+ * @template TCarry
+ * @psalm-param callable(TCarry, TValue): TCarry $callback
+ * @psalm-param TCarry $initial
+ * @psalm-return TCarry
+ */
+ public function reduce(callable $callback, $initial = null)
+ {
+ return array_reduce($this->array, $callback, $initial);
+ }
+
+ /**
+ * @throws \OutOfRangeException if the index is not in the range [0, size-1]
+ *
+ * @psalm-return TValue
+ */
+ public function remove(int $index)
+ {
+ if ( ! $this->validIndex($index)) {
+ throw new OutOfRangeException();
+ }
+
+ $value = array_splice($this->array, $index, 1, null)[0];
+ $this->checkCapacity();
+
+ return $value;
+ }
+
+ /**
+ *
+ */
+ public function reverse()
+ {
+ $this->array = array_reverse($this->array);
+ }
+
+ /**
+ * @psalm-return Sequence
+ */
+ public function reversed(): Sequence
+ {
+ return new self(array_reverse($this->array));
+ }
+
+ /**
+ * Converts negative or large rotations into the minimum positive number
+ * of rotations required to rotate the sequence by a given $r.
+ */
+ private function normalizeRotations(int $r)
+ {
+ $n = count($this);
+
+ if ($n < 2) return 0;
+ if ($r < 0) return $n - (abs($r) % $n);
+
+ return $r % $n;
+ }
+
+ /**
+ *
+ */
+ public function rotate(int $rotations)
+ {
+ for ($r = $this->normalizeRotations($rotations); $r > 0; $r--) {
+ array_push($this->array, array_shift($this->array));
+ }
+ }
+
+ /**
+ * @throws \OutOfRangeException if the index is not in the range [0, size-1]
+ *
+ * @psalm-param TValue $value
+ */
+ public function set(int $index, $value)
+ {
+ if ( ! $this->validIndex($index)) {
+ throw new OutOfRangeException();
+ }
+
+ $this->array[$index] = $value;
+ }
+
+ /**
+ * @throws \UnderflowException if the sequence was empty.
+ *
+ * @psalm-return TValue
+ */
+ public function shift()
+ {
+ if ($this->isEmpty()) {
+ throw new UnderflowException();
+ }
+
+ $value = array_shift($this->array);
+ $this->checkCapacity();
+
+ return $value;
+ }
+
+ /**
+ * @psalm-return Sequence
+ */
+ public function slice(int $offset, int $length = null): Sequence
+ {
+ if (func_num_args() === 1) {
+ $length = count($this);
+ }
+
+ return new self(array_slice($this->array, $offset, $length));
+ }
+
+ /**
+ * @psalm-param (callable(TValue, TValue): int)|null $comparator
+ */
+ public function sort(callable $comparator = null)
+ {
+ if ($comparator) {
+ usort($this->array, $comparator);
+ } else {
+ sort($this->array);
+ }
+ }
+
+ /**
+ * @psalm-param (callable(TValue, TValue): int)|null $comparator
+ * @psalm-return Sequence
+ */
+ public function sorted(callable $comparator = null): Sequence
+ {
+ $copy = $this->copy();
+ $copy->sort($comparator);
+ return $copy;
+ }
+
+ /**
+ * @return int|float
+ */
+ public function sum()
+ {
+ return array_sum($this->array);
+ }
+
+ /**
+ * @psalm-param TValue ...$values
+ */
+ public function unshift(...$values)
+ {
+ if ($values) {
+ $this->array = array_merge($values, $this->array);
+ $this->checkCapacity();
+ }
+ }
+
+ /**
+ *
+ */
+ private function validIndex(int $index)
+ {
+ return $index >= 0 && $index < count($this);
+ }
+
+ /**
+ *
+ */
+ #[\ReturnTypeWillChange]
+ public function getIterator()
+ {
+ foreach ($this->array as $value) {
+ yield $value;
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function clear()
+ {
+ $this->array = [];
+ $this->capacity = self::MIN_CAPACITY;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ if ($offset === null) {
+ $this->push($value);
+ } else {
+ $this->set($offset, $value);
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ #[\ReturnTypeWillChange]
+ public function &offsetGet($offset)
+ {
+ if ( ! $this->validIndex($offset)) {
+ throw new OutOfRangeException();
+ }
+
+ return $this->array[$offset];
+ }
+
+ /**
+ * @inheritdoc
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ if (is_integer($offset) && $this->validIndex($offset)) {
+ $this->remove($offset);
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ return is_integer($offset)
+ && $this->validIndex($offset)
+ && $this->get($offset) !== null;
+ }
+}
diff --git a/packages/php-ds/src/Traits/SquaredCapacity.php b/packages/php-ds/src/Traits/SquaredCapacity.php
new file mode 100644
index 0000000..01bd25a
--- /dev/null
+++ b/packages/php-ds/src/Traits/SquaredCapacity.php
@@ -0,0 +1,58 @@
+capacity = max($this->square($capacity), $this->capacity);
+ }
+
+ /**
+ * Called when capacity should be increased to accommodate new values.
+ */
+ protected function increaseCapacity()
+ {
+ $this->capacity = $this->square(
+ max(
+ count($this) + 1,
+ $this->capacity * $this->getGrowthFactor()
+ )
+ );
+ }
+
+ /**
+ * @param int $total
+ */
+ protected function ensureCapacity(int $total)
+ {
+ while ($total > $this->capacity()) {
+ $this->increaseCapacity();
+ }
+ }
+}
diff --git a/packages/php-ds/src/Vector.php b/packages/php-ds/src/Vector.php
new file mode 100644
index 0000000..e69d195
--- /dev/null
+++ b/packages/php-ds/src/Vector.php
@@ -0,0 +1,37 @@
+
+ * @template-use Traits\GenericCollection
+ * @template-use Traits\GenericSequence
+ */
+final class Vector implements Sequence
+{
+ use Traits\GenericCollection;
+ use Traits\GenericSequence;
+ use Traits\Capacity;
+
+ public const MIN_CAPACITY = 8;
+
+ protected function getGrowthFactor(): float
+ {
+ return 1.5;
+ }
+
+ /**
+ * @return bool whether capacity should be increased.
+ */
+ protected function shouldIncreaseCapacity(): bool
+ {
+ return count($this) > $this->capacity;
+ }
+}
diff --git a/packages/php-ds/tests/bootstrap.php b/packages/php-ds/tests/bootstrap.php
new file mode 100644
index 0000000..8fc0574
--- /dev/null
+++ b/packages/php-ds/tests/bootstrap.php
@@ -0,0 +1,7 @@
+