From bc566a70107ad932f24d6aca55bd2980f6508bd9 Mon Sep 17 00:00:00 2001 From: azjezz Date: Mon, 1 Jul 2024 23:08:41 +0100 Subject: [PATCH] feat(collections): introduce `Set`, `SetInterface`, `MutableSet`, and `MutableSetInterface` Signed-off-by: azjezz --- docs/component/collection.md | 6 +- .../AccessibleCollectionInterface.php | 47 +- src/Psl/Collection/CollectionInterface.php | 47 +- .../Exception/InvalidOffsetException.php | 11 + .../Exception/OutOfBoundsException.php | 2 +- .../Collection/Exception/RuntimeException.php | 11 + src/Psl/Collection/IndexAccessInterface.php | 4 +- src/Psl/Collection/Map.php | 4 +- .../MutableAccessibleCollectionInterface.php | 140 ++-- .../Collection/MutableCollectionInterface.php | 47 +- .../MutableIndexAccessInterface.php | 28 - src/Psl/Collection/MutableMap.php | 99 ++- src/Psl/Collection/MutableMapInterface.php | 4 +- src/Psl/Collection/MutableSet.php | 717 ++++++++++++++++++ src/Psl/Collection/MutableSetInterface.php | 374 +++++++++ src/Psl/Collection/MutableVector.php | 104 ++- src/Psl/Collection/MutableVectorInterface.php | 4 +- src/Psl/Collection/Set.php | 553 ++++++++++++++ src/Psl/Collection/SetInterface.php | 337 ++++++++ src/Psl/Collection/Vector.php | 4 +- src/Psl/Collection/VectorInterface.php | 4 +- src/Psl/Internal/Loader.php | 10 + src/Psl/Type/Internal/MutableSetType.php | 126 +++ src/Psl/Type/Internal/SetType.php | 126 +++ src/Psl/Type/mutable_set.php | 21 + src/Psl/Type/set.php | 21 + tests/unit/Collection/AbstractSetTest.php | 534 +++++++++++++ tests/unit/Collection/AbstractVectorTest.php | 6 +- tests/unit/Collection/MutableMapTest.php | 93 +++ tests/unit/Collection/MutableSetTest.php | 205 +++++ tests/unit/Collection/MutableVectorTest.php | 98 ++- tests/unit/Collection/SetTest.php | 29 + tests/unit/Collection/VectorTest.php | 6 +- tests/unit/Type/MutableSetTypeTest.php | 195 +++++ tests/unit/Type/SetTypeTest.php | 191 +++++ 35 files changed, 3932 insertions(+), 276 deletions(-) create mode 100644 src/Psl/Collection/Exception/InvalidOffsetException.php create mode 100644 src/Psl/Collection/Exception/RuntimeException.php create mode 100644 src/Psl/Collection/MutableSet.php create mode 100644 src/Psl/Collection/MutableSetInterface.php create mode 100644 src/Psl/Collection/Set.php create mode 100644 src/Psl/Collection/SetInterface.php create mode 100644 src/Psl/Type/Internal/MutableSetType.php create mode 100644 src/Psl/Type/Internal/SetType.php create mode 100644 src/Psl/Type/mutable_set.php create mode 100644 src/Psl/Type/set.php create mode 100644 tests/unit/Collection/AbstractSetTest.php create mode 100644 tests/unit/Collection/MutableSetTest.php create mode 100644 tests/unit/Collection/SetTest.php create mode 100644 tests/unit/Type/MutableSetTypeTest.php create mode 100644 tests/unit/Type/SetTypeTest.php diff --git a/docs/component/collection.md b/docs/component/collection.md index 05b4bd5d..5de16f73 100644 --- a/docs/component/collection.md +++ b/docs/component/collection.md @@ -16,18 +16,22 @@ - [CollectionInterface](./../../src/Psl/Collection/CollectionInterface.php#L23) - [IndexAccessInterface](./../../src/Psl/Collection/IndexAccessInterface.php#L13) - [MapInterface](./../../src/Psl/Collection/MapInterface.php#L15) -- [MutableAccessibleCollectionInterface](./../../src/Psl/Collection/MutableAccessibleCollectionInterface.php#L22) +- [MutableAccessibleCollectionInterface](./../../src/Psl/Collection/MutableAccessibleCollectionInterface.php#L23) - [MutableCollectionInterface](./../../src/Psl/Collection/MutableCollectionInterface.php#L22) - [MutableIndexAccessInterface](./../../src/Psl/Collection/MutableIndexAccessInterface.php#L16) - [MutableMapInterface](./../../src/Psl/Collection/MutableMapInterface.php#L16) +- [MutableSetInterface](./../../src/Psl/Collection/MutableSetInterface.php#L15) - [MutableVectorInterface](./../../src/Psl/Collection/MutableVectorInterface.php#L15) +- [SetInterface](./../../src/Psl/Collection/SetInterface.php#L14) - [VectorInterface](./../../src/Psl/Collection/VectorInterface.php#L14) #### `Classes` - [Map](./../../src/Psl/Collection/Map.php#L25) - [MutableMap](./../../src/Psl/Collection/MutableMap.php#L25) +- [MutableSet](./../../src/Psl/Collection/MutableSet.php#L23) - [MutableVector](./../../src/Psl/Collection/MutableVector.php#L23) +- [Set](./../../src/Psl/Collection/Set.php#L23) - [Vector](./../../src/Psl/Collection/Vector.php#L22) diff --git a/src/Psl/Collection/AccessibleCollectionInterface.php b/src/Psl/Collection/AccessibleCollectionInterface.php index 8f1e2a89..ad37ea53 100644 --- a/src/Psl/Collection/AccessibleCollectionInterface.php +++ b/src/Psl/Collection/AccessibleCollectionInterface.php @@ -43,7 +43,7 @@ public function keys(): AccessibleCollectionInterface; * that meet a supplied condition. * * Only values that meet a certain criteria are affected by a call to - * `filter()`, while all values are affected by a call to `map()`. + * `filter()`. * * The keys associated with the current `AccessibleCollectionInterface` remain unchanged in the * returned `AccessibleCollectionInterface`. @@ -61,8 +61,7 @@ public function filter(Closure $fn): AccessibleCollectionInterface; * that meet a supplied condition applied to its keys and values. * * Only keys and values that meet a certain criteria are affected by a call - * to `filterWithKey()`, while all values are affected by a call to - * `mapWithKey()`. + * to `filterWithKey()`. * * The keys associated with the current `AccessibleCollectionInterface` remain unchanged in the * returned `AccessibleCollectionInterface`; the keys will be used in the filtering process only. @@ -76,48 +75,6 @@ public function filter(Closure $fn): AccessibleCollectionInterface; */ public function filterWithKey(Closure $fn): AccessibleCollectionInterface; - /** - * Returns a `AccessibleCollectionInterface` after an operation has been applied to each value - * in the current `AccessibleCollectionInterface`. - * - * Every value in the current Map is affected by a call to `map()`, unlike - * `filter()` where only values that meet a certain criteria are affected. - * - * The keys will remain unchanged from the current `AccessibleCollectionInterface` to the - * returned `AccessibleCollectionInterface`. - * - * @template Tu - * - * @param (Closure(Tv): Tu) $fn The callback containing the operation to apply to the current - * `AccessibleCollectionInterface` values. - * - * @return AccessibleCollectionInterface A `AccessibleCollectionInterface` containing key/value - * pairs after a user-specified operation is applied. - */ - public function map(Closure $fn): AccessibleCollectionInterface; - - /** - * Returns a `AccessibleCollectionInterface` after an operation has been applied to each key and - * value in the current `AccessibleCollectionInterface`. - * - * Every key and value in the current `AccessibleCollectionInterface` is affected by a call to - * `mapWithKey()`, unlike `filterWithKey()` where only values that meet a - * certain criteria are affected. - * - * The keys will remain unchanged from this `AccessibleCollectionInterface` to the returned - * `AccessibleCollectionInterface`. The keys are only used to help in the mapping operation. - * - * @template Tu - * - * @param (Closure(Tk, Tv): Tu) $fn The callback containing the operation to apply to the current - * `AccessibleCollectionInterface` keys and values. - * - * @return AccessibleCollectionInterface A `AccessibleCollectionInterface` containing the values - * after a user-specified operation on the current - * `AccessibleCollectionInterface`'s keys and values is applied. - */ - public function mapWithKey(Closure $fn): AccessibleCollectionInterface; - /** * Returns the first value in the current `AccessibleCollectionInterface`. * diff --git a/src/Psl/Collection/CollectionInterface.php b/src/Psl/Collection/CollectionInterface.php index a79fb165..87d63df5 100644 --- a/src/Psl/Collection/CollectionInterface.php +++ b/src/Psl/Collection/CollectionInterface.php @@ -60,8 +60,7 @@ public function jsonSerialize(): array; * Returns a `CollectionInterface` containing the values of the current `CollectionInterface` * that meet a supplied condition. * - * Only values that meet a certain criteria are affected by a call to - * `filter()`, while all values are affected by a call to `map()`. + * Only values that meet a certain criteria are affected by a call to `filter()`. * * The keys associated with the current `CollectionInterface` remain unchanged in the * returned `CollectionInterface`. @@ -79,8 +78,7 @@ public function filter(Closure $fn): CollectionInterface; * that meet a supplied condition applied to its keys and values. * * Only keys and values that meet a certain criteria are affected by a call - * to `filterWithKey()`, while all values are affected by a call to - * `mapWithKey()`. + * to `filterWithKey()`. * * The keys associated with the current `CollectionInterface` remain unchanged in the * returned `CollectionInterface`; the keys will be used in the filtering process only. @@ -94,47 +92,6 @@ public function filter(Closure $fn): CollectionInterface; */ public function filterWithKey(Closure $fn): CollectionInterface; - /** - * Returns a `CollectionInterface` after an operation has been applied to each value - * in the current `CollectionInterface`. - * - * Every value in the current Map is affected by a call to `map()`, unlike - * `filter()` where only values that meet a certain criteria are affected. - * - * The keys will remain unchanged from the current `CollectionInterface` to the - * returned `CollectionInterface`. - * - * @template Tu - * - * @param (Closure(Tv): Tu) $fn The callback containing the operation to apply to the current - * `CollectionInterface` values. - * - * @return CollectionInterface A `CollectionInterface` containing key/value pairs after - * a user-specified operation is applied. - */ - public function map(Closure $fn): CollectionInterface; - - /** - * Returns a `CollectionInterface` after an operation has been applied to each key and - * value in the current `CollectionInterface`. - * - * Every key and value in the current `CollectionInterface` is affected by a call to - * `mapWithKey()`, unlike `filterWithKey()` where only values that meet a - * certain criteria are affected. - * - * The keys will remain unchanged from this `CollectionInterface` to the returned - * `CollectionInterface`. The keys are only used to help in the mapping operation. - * - * @template Tu - * - * @param (Closure(Tk, Tv): Tu) $fn The callback containing the operation to apply to the current - * `CollectionInterface` keys and values. - * - * @return CollectionInterface A `CollectionInterface` containing the values after a user-specified - * operation on the current `CollectionInterface`'s keys and values is applied. - */ - public function mapWithKey(Closure $fn): CollectionInterface; - /** * Returns a `CollectionInterface` where each element is a `array{0: Tv, 1: Tu}` that combines the * element of the current `CollectionInterface` and the provided elements array. diff --git a/src/Psl/Collection/Exception/InvalidOffsetException.php b/src/Psl/Collection/Exception/InvalidOffsetException.php new file mode 100644 index 00000000..8f257788 --- /dev/null +++ b/src/Psl/Collection/Exception/InvalidOffsetException.php @@ -0,0 +1,11 @@ +elements)) { throw Exception\OutOfBoundsException::for($k); @@ -249,7 +249,7 @@ public function contains(int|string $k): bool * * @psalm-mutation-free */ - public function get(string|int $k): mixed + public function get(int|string $k): mixed { return $this->elements[$k] ?? null; } diff --git a/src/Psl/Collection/MutableAccessibleCollectionInterface.php b/src/Psl/Collection/MutableAccessibleCollectionInterface.php index 902f8370..c3355724 100644 --- a/src/Psl/Collection/MutableAccessibleCollectionInterface.php +++ b/src/Psl/Collection/MutableAccessibleCollectionInterface.php @@ -4,11 +4,11 @@ namespace Psl\Collection; +use ArrayAccess; use Closure; /** - * The base interface implemented for a collection type that you are able set and remove its values. - * keys. + * The base interface implemented for a collection type whose values you are able to set and remove. * * Every concrete mutable class indirectly implements this interface. * @@ -18,9 +18,11 @@ * @extends AccessibleCollectionInterface * @extends MutableCollectionInterface * @extends MutableIndexAccessInterface + * @extends ArrayAccess */ interface MutableAccessibleCollectionInterface extends AccessibleCollectionInterface, + ArrayAccess, MutableCollectionInterface, MutableIndexAccessInterface { @@ -49,7 +51,7 @@ public function keys(): MutableAccessibleCollectionInterface; * `MutableAccessibleCollectionInterface` that meet a supplied condition. * * Only values that meet a certain criteria are affected by a call to - * `filter()`, while all values are affected by a call to `map()`. + * `filter()`. * * The keys associated with the current `MutableAccessibleCollectionInterface` remain unchanged in the * returned `MutableAccessibleCollectionInterface`. @@ -67,8 +69,7 @@ public function filter(Closure $fn): MutableAccessibleCollectionInterface; * `MutableAccessibleCollectionInterface` that meet a supplied condition applied to its keys and values. * * Only keys and values that meet a certain criteria are affected by a call - * to `filterWithKey()`, while all values are affected by a call to - * `mapWithKey()`. + * to `filterWithKey()`. * * The keys associated with the current `MutableAccessibleCollectionInterface` remain unchanged in the * returned `MutableAccessibleCollectionInterface`; the keys will be used in the filtering process only. @@ -83,83 +84,6 @@ public function filter(Closure $fn): MutableAccessibleCollectionInterface; */ public function filterWithKey(Closure $fn): MutableAccessibleCollectionInterface; - /** - * Returns a `MutableAccessibleCollectionInterface` after an operation has been applied to each value - * in the current `MutableAccessibleCollectionInterface`. - * - * Every value in the current Map is affected by a call to `map()`, unlike - * `filter()` where only values that meet a certain criteria are affected. - * - * The keys will remain unchanged from the current `MutableAccessibleCollectionInterface` to the - * returned `MutableAccessibleCollectionInterface`. - * - * @template Tu - * - * @param (Closure(Tv): Tu) $fn The callback containing the operation to apply to the current - * `MutableAccessibleCollectionInterface` values. - * - * @return MutableAccessibleCollectionInterface A `MutableAccessibleCollectionInterface` containing - * key/value pairs after a user-specified operation is applied. - */ - public function map(Closure $fn): MutableAccessibleCollectionInterface; - - /** - * Returns a `MutableAccessibleCollectionInterface` after an operation has been applied to each key and - * value in the current `MutableAccessibleCollectionInterface`. - * - * Every key and value in the current `MutableAccessibleCollectionInterface` is affected by a call to - * `mapWithKey()`, unlike `filterWithKey()` where only values that meet a - * certain criteria are affected. - * - * The keys will remain unchanged from this `MutableAccessibleCollectionInterface` to the returned - * `MutableAccessibleCollectionInterface`. The keys are only used to help in the mapping operation. - * - * @template Tu - * - * @param (Closure(Tk, Tv): Tu) $fn The callback containing the operation to apply to the current - * `MutableAccessibleCollectionInterface` keys and values. - * - * @return MutableAccessibleCollectionInterface A `MutableAccessibleCollectionInterface` containing - * the values after a user-specified operation on the current - * `MutableAccessibleCollectionInterface`'s keys and values is - * applied. - */ - public function mapWithKey(Closure $fn): MutableAccessibleCollectionInterface; - - /** - * Stores a value into the current collection with the specified key, - * overwriting the previous value associated with the key. - * - * If the key is not present, an exception is thrown. If you want to add - * a value even if a key is not present, use `add()`. - * - * It returns the current collection, meaning changes made to the current - * collection will be reflected in the returned collection. - * - * @param Tk $k The key to which we will set the value. - * @param Tv $v The value to set. - * - * @return MutableAccessibleCollectionInterface Returns itself. - */ - public function set(int|string $k, mixed $v): MutableAccessibleCollectionInterface; - - /** - * For every element in the provided elements, stores a value into the - * current collection associated with each key, overwriting the previous value - * associated with the key. - * - * If the key is not present, an exception is thrown. If you want to add - * a value even if a key is not present, use `addAll()`. - * - * It the current collection, meaning changes made to the current collection - * will be reflected in the returned collection. - * - * @param array $elements The elements with the new values to set. - * - * @return MutableAccessibleCollectionInterface Returns itself. - */ - public function setAll(array $elements): MutableAccessibleCollectionInterface; - /** * Removes the specified key (and associated value) from the current * collection. @@ -315,4 +239,56 @@ public function slice(int $start, ?int $length = null): MutableAccessibleCollect * @psalm-mutation-free */ public function chunk(int $size): MutableAccessibleCollectionInterface; + + /** + * Determines if the specified offset exists in the current collection. + * + * @param mixed $offset An offset to check for. + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + * + * @return bool Returns true if the specified offset exists, false otherwise. + * + * @psalm-assert-if-true Tk $offset + * + * @psalm-mutation-free + */ + public function offsetExists(mixed $offset): bool; + + /** + * Returns the value at the specified offset. + * + * @param mixed $offset The offset to retrieve. + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + * @throws Exception\OutOfBoundsException If the offset is out-of-bounds. + * + * @return Tv The value at the specified offset. + * + * @psalm-mutation-free + */ + public function offsetGet(mixed $offset): mixed; + + /** + * Sets the value at the specified offset. + * + * @param mixed $offset The offset to assign the value to. + * @param Tv $value The value to set. + * + * @psalm-external-mutation-free + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + */ + public function offsetSet(mixed $offset, mixed $value): void; + + /** + * Unsets the value at the specified offset. + * + * @param mixed $offset The offset to unset. + * + * @psalm-external-mutation-free + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + */ + public function offsetUnset(mixed $offset): void; } diff --git a/src/Psl/Collection/MutableCollectionInterface.php b/src/Psl/Collection/MutableCollectionInterface.php index cee7cfe5..0e5149d6 100644 --- a/src/Psl/Collection/MutableCollectionInterface.php +++ b/src/Psl/Collection/MutableCollectionInterface.php @@ -26,7 +26,7 @@ interface MutableCollectionInterface extends CollectionInterface * that meet a supplied condition. * * Only values that meet a certain criteria are affected by a call to - * `filter()`, while all values are affected by a call to `map()`. + * `filter()`. * * The keys associated with the current `MutableCollectionInterface` remain unchanged in the * returned `MutableCollectionInterface`. @@ -44,8 +44,7 @@ public function filter(Closure $fn): MutableCollectionInterface; * that meet a supplied condition applied to its keys and values. * * Only keys and values that meet a certain criteria are affected by a call - * to `filterWithKey()`, while all values are affected by a call to - * `mapWithKey()`. + * to `filterWithKey()`. * * The keys associated with the current `MutableCollectionInterface` remain unchanged in the * returned `MutableCollectionInterface`; the keys will be used in the filtering process only. @@ -59,48 +58,6 @@ public function filter(Closure $fn): MutableCollectionInterface; */ public function filterWithKey(Closure $fn): MutableCollectionInterface; - /** - * Returns a `MutableCollectionInterface` after an operation has been applied to each value - * in the current `MutableCollectionInterface`. - * - * Every value in the current Map is affected by a call to `map()`, unlike - * `filter()` where only values that meet a certain criteria are affected. - * - * The keys will remain unchanged from the current `MutableCollectionInterface` to the - * returned `MutableCollectionInterface`. - * - * @template Tu - * - * @param (Closure(Tv): Tu) $fn The callback containing the operation to apply to the current - * `MutableCollectionInterface` values. - * - * @return MutableCollectionInterface A `MutableCollectionInterface` containing key/value pairs - * after a user-specified operation is applied. - */ - public function map(Closure $fn): MutableCollectionInterface; - - /** - * Returns a `MutableCollectionInterface` after an operation has been applied to each key and - * value in the current `MutableCollectionInterface`. - * - * Every key and value in the current `MutableCollectionInterface` is affected by a call to - * `mapWithKey()`, unlike `filterWithKey()` where only values that meet a - * certain criteria are affected. - * - * The keys will remain unchanged from this `MutableCollectionInterface` to the returned - * `MutableCollectionInterface`. The keys are only used to help in the mapping operation. - * - * @template Tu - * - * @param (Closure(Tk, Tv): Tu) $fn The callback containing the operation to apply to the current - * `MutableCollectionInterface` keys and values. - * - * @return MutableCollectionInterface A `MutableCollectionInterface` containing the values - * after a user-specified operation on the current - * `MutableCollectionInterface`'s keys and values is applied. - */ - public function mapWithKey(Closure $fn): MutableCollectionInterface; - /** * Returns a `MutableCollectionInterface` where each element is a `array{0: Tv, 1: Tu}` that combines the * element of the current `MutableCollectionInterface` and the provided elements. diff --git a/src/Psl/Collection/MutableIndexAccessInterface.php b/src/Psl/Collection/MutableIndexAccessInterface.php index 2cca573c..977f1b00 100644 --- a/src/Psl/Collection/MutableIndexAccessInterface.php +++ b/src/Psl/Collection/MutableIndexAccessInterface.php @@ -15,34 +15,6 @@ */ interface MutableIndexAccessInterface extends IndexAccessInterface { - /** - * Stores a value into the current collection with the specified key, - * overwriting the previous value associated with the key. - * - * It returns the current collection, meaning changes made to the current - * collection will be reflected in the returned collection. - * - * @param Tk $k The key to which we will set the value - * @param Tv $v The value to set - * - * @return MutableIndexAccessInterface Returns itself - */ - public function set(int|string $k, mixed $v): MutableIndexAccessInterface; - - /** - * For every element in the provided elements array, stores a value into the - * current collection associated with each key, overwriting the previous value - * associated with the key. - * - * It the current collection, meaning changes made to the current collection - * will be reflected in the returned collection. - * - * @param array $elements The elements with the new values to set - * - * @return MutableIndexAccessInterface Returns itself - */ - public function setAll(array $elements): MutableIndexAccessInterface; - /** * Removes the specified key (and associated value) from the current * collection. diff --git a/src/Psl/Collection/MutableMap.php b/src/Psl/Collection/MutableMap.php index 581a2286..cbe40aca 100644 --- a/src/Psl/Collection/MutableMap.php +++ b/src/Psl/Collection/MutableMap.php @@ -219,7 +219,7 @@ public function jsonSerialize(): array * * @psalm-mutation-free */ - public function at(string|int $k): mixed + public function at(int|string $k): mixed { if (!array_key_exists($k, $this->elements)) { throw Exception\OutOfBoundsException::for($k); @@ -249,7 +249,7 @@ public function contains(int|string $k): bool * * @psalm-mutation-free */ - public function get(string|int $k): mixed + public function get(int|string $k): mixed { return $this->elements[$k] ?? null; } @@ -635,13 +635,13 @@ public function add(int|string $k, mixed $v): MutableMap /** * For every element in the provided elements, add the value into the current map. * - * @param array $elements The elements with the new values to add. + * @param iterable $elements The elements with the new values to add. * * @return MutableMap Returns itself. * * @psalm-external-mutation-free */ - public function addAll(array $elements): MutableMap + public function addAll(iterable $elements): MutableMap { foreach ($elements as $k => $v) { $this->add($k, $v); @@ -688,4 +688,95 @@ public function clear(): MutableMap return $this; } + + /** + * Determines if the specified offset exists in the current map. + * + * @param mixed $offset An offset to check for. + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + * + * @return bool Returns true if the specified offset exists, false otherwise. + * + * @psalm-assert array-key $offset + * + * @psalm-mutation-free + */ + public function offsetExists(mixed $offset): bool + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\InvalidOffsetException('Invalid map read offset type, expected a string or an integer.'); + } + + /** @var Tk $offset - technically, we don't know if the offset is of type Tk, but we can assume it is, as this causes no "harm". */ + return $this->contains($offset); + } + + /** + * Returns the value at the specified offset. + * + * @param mixed $offset The offset to retrieve. + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + * @throws Exception\OutOfBoundsException If the offset is out-of-bounds. + * + * @return Tv|null The value at the specified offset, null if the offset does not exist. + * + * @psalm-mutation-free + * + * @psalm-assert array-key $offset + */ + public function offsetGet(mixed $offset): mixed + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\InvalidOffsetException('Invalid map read offset type, expected a string or an integer.'); + } + + /** @var Tk $offset - technically, we don't know if the offset is of type Tk, but we can assume it is, as this causes no "harm". */ + return $this->at($offset); + } + + /** + * Sets the value at the specified offset. + * + * @param mixed $offset The offset to assign the value to. + * @param Tv $value The value to set. + * + * @psalm-external-mutation-free + * + * @psalm-assert Tk $offset + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + * @throws Exception\OutOfBoundsException If the offset is out-of-bounds. + */ + public function offsetSet(mixed $offset, mixed $value): void + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\InvalidOffsetException('Invalid map write offset type, expected a string or an integer.'); + } + + /** @var Tk $offset - technically, we don't know if the offset is of type Tk, but we can assume it is, as this causes no "harm". */ + $this->add($offset, $value); + } + + /** + * Unsets the value at the specified offset. + * + * @param mixed $offset The offset to unset. + * + * @psalm-external-mutation-free + * + * @psalm-assert array-key $offset + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + */ + public function offsetUnset(mixed $offset): void + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\InvalidOffsetException('Invalid map read offset type, expected a string or an integer.'); + } + + /** @var Tk $offset - technically, we don't know if the offset is of type Tk, but we can assume it is, as this causes no "harm". */ + $this->remove($offset); + } } diff --git a/src/Psl/Collection/MutableMapInterface.php b/src/Psl/Collection/MutableMapInterface.php index 609f24e3..1c696f3f 100644 --- a/src/Psl/Collection/MutableMapInterface.php +++ b/src/Psl/Collection/MutableMapInterface.php @@ -342,11 +342,11 @@ public function add(int|string $k, mixed $v): MutableMapInterface; /** * For every element in the provided elements array, add the value into the current collection. * - * @param array $elements The elements with the new values to add. + * @param iterable $elements The elements with the new values to add. * * @return MutableMapInterface Returns itself. */ - public function addAll(array $elements): MutableMapInterface; + public function addAll(iterable $elements): MutableMapInterface; /** * Removes the specified key (and associated value) from the current diff --git a/src/Psl/Collection/MutableSet.php b/src/Psl/Collection/MutableSet.php new file mode 100644 index 00000000..edf6e69d --- /dev/null +++ b/src/Psl/Collection/MutableSet.php @@ -0,0 +1,717 @@ + + */ +final class MutableSet implements MutableSetInterface +{ + /** + * @var array + */ + private array $elements = []; + + /** + * Creates a new `MutableSet` containing the values of the given array. + * + * @param array $elements + * + * @psalm-mutation-free + */ + public function __construct(array $elements) + { + $set = []; + foreach ($elements as $element) { + $set[$element] = $element; + } + + $this->elements = $set; + } + + /** + * Creates and returns a default instance of {@see MutableSet}. + * + * @return static A default instance of {@see MutableSet}. + * + * @psalm-external-mutation-free + */ + public static function default(): static + { + return new self([]); + } + + /** + * Create a set from the given array, using the values of the array as the set values. + * + * @template Ts of array-key + * + * @param array $elements + * + * @return MutableSet + * + * @pure + */ + public static function fromArray(array $elements): MutableSet + { + return new self($elements); + } + + /** + * Create a set from the given $elements array, using the keys of the array as the set values. + * + * @template Ts of array-key + * + * @param array $elements + * + * @return MutableSet + * + * @pure + */ + public static function fromArrayKeys(array $elements): MutableSet + { + /** @var array $set */ + $set = []; + foreach ($elements as $element => $_) { + $set[$element] = $element; + } + + return new self($set); + } + + /** + * Returns the first value in the current `MutableSet`. + * + * @return T|null The first value in the current `MutableSet`, or `null` if the + * current `MutableSet` is empty. + * + * @psalm-mutation-free + */ + public function first(): null|int|string + { + return array_key_first($this->elements); + } + + /** + * Returns the last value in the current `MutableSet`. + * + * @return T|null The last value in the current `MutableSet`, or `null` if the + * current `MutableSet` is empty. + * + * @psalm-mutation-free + */ + public function last(): null|int|string + { + return array_key_last($this->elements); + } + + /** + * Retrieve an external iterator. + * + * @return Iter\Iterator + */ + public function getIterator(): Iter\Iterator + { + return Iter\Iterator::create($this->elements); + } + + /** + * Is the set empty? + * + * @psalm-mutation-free + */ + public function isEmpty(): bool + { + return [] === $this->elements; + } + + /** + * Get the number of elements in the current `MutableSet`. + * + * @psalm-mutation-free + * + * @return int<0, max> + */ + public function count(): int + { + /** @var int<0, max> */ + return count($this->elements); + } + + /** + * Get an array copy of the current `MutableSet`. + * + * @return array + * + * @psalm-mutation-free + */ + public function toArray(): array + { + return $this->elements; + } + + /** + * Get an array copy of the current `MutableSet`. + * + * @return array + * + * @psalm-mutation-free + */ + public function jsonSerialize(): array + { + return $this->elements; + } + + /** + * Returns the provided value if it exists in the current `MutableSet`. + * + * As {@see MutableSet} does not have keys, this method checks if the value exists in the set. + * If the value exists, it is returned to indicate presence in the set. If the value does not exist, + * an {@see Exception\OutOfBoundsException} is thrown to indicate the absence of the value. + * + * @param T $k + * + * @throws Exception\OutOfBoundsException If $k is out-of-bounds. + * + * @return T + * + * @psalm-mutation-free + */ + public function at(int|string $k): int|string + { + if (!array_key_exists($k, $this->elements)) { + throw Exception\OutOfBoundsException::for($k); + } + + // the key exists, and we know it's the same as the value. + return $k; + } + + /** + * Determines if the specified value is in the current set. + * + * As {@see MutableSet} does not have keys, this method checks if the value exists in the set. + * If the value exists, it returns true to indicate presence in the set. If the value does not exist, + * it returns false to indicate the absence of the value. + * + * @param T $k + * + * @return bool True if the value is in the set, false otherwise. + * + * @psalm-mutation-free + */ + public function contains(int|string $k): bool + { + return array_key_exists($k, $this->elements); + } + + /** + * Returns the provided value if it is part of the set, or null if it is not. + * + * As {@see MutableSet} does not have keys, this method checks if the value exists in the set. + * If the value exists, it is returned to indicate presence in the set. If the value does not exist, + * null is returned to indicate the absence of the value. + * + * @param T $k + * + * @return T|null + * + * @psalm-mutation-free + */ + public function get(int|string $k): null|int|string + { + return $this->elements[$k] ?? null; + } + + /** + * Returns the first key in the current `MutableSet`. + * + * As {@see MutableSet} does not have keys, this method acts as an alias for {@see MutableSet::first()}. + * + * @return T|null The first value in the current `MutableSet`, or `null` if the + * current `MutableSet` is empty. + * + * @psalm-mutation-free + */ + public function firstKey(): null|int|string + { + return $this->first(); + } + + /** + * Returns the last key in the current `MutableSet`. + * + * As {@see MutableSet} does not have keys, this method acts as an alias for {@see MutableSet::last()}. + * + * @return T|null The last value in the current `MutableSet`, or `null` if the + * current `MutableSet` is empty. + * + * @psalm-mutation-free + */ + public function lastKey(): null|int|string + { + return $this->last(); + } + + /** + * Returns the key of the first element that matches the search value. + * + * If no element matches the search value, this function returns null. + * + * As {@see MutableSet} does not have keys, this method returns the value itself. + * + * @param T $search_value The value that will be search for in the current + * `MutableSet`. + * + * @return T|null The value if its found, null otherwise. + * + * @psalm-mutation-free + */ + public function linearSearch(mixed $search_value): null|int|string + { + foreach ($this->elements as $element) { + if ($search_value === $element) { + return $element; + } + } + + return null; + } + + /** + * Removes the specified value from the current set. + * + * If the value is not in the current set, the current set is unchanged. + * + * @param T $k The value to remove. + * + * @return MutableSet Returns itself. + */ + public function remove(int|string $k): MutableSet + { + unset($this->elements[$k]); + + return $this; + } + + /** + * Removes all elements from the set. + * + * @return MutableSet Returns itself + * + * @psalm-external-mutation-free + */ + public function clear(): MutableSet + { + $this->elements = []; + + return $this; + } + + /** + * Add a value to the set and return the set itself. + * + * @param T $v The value to add. + * + * @return MutableSet Returns itself. + * + * @psalm-external-mutation-free + */ + public function add(mixed $v): MutableSet + { + $this->elements[$v] = $v; + + return $this; + } + + /** + * For every element in the provided elements iterable, add the value into the current set. + * + * @param iterable $elements The elements with the new values to add + * + * @return MutableSet returns itself. + * + * @psalm-external-mutation-free + */ + public function addAll(iterable $elements): MutableSet + { + foreach ($elements as $item) { + $this->add($item); + } + + return $this; + } + + /** + * Returns a `MutableVector` containing the values of the current `MutableSet`. + * + * @return MutableVector + * + * @psalm-mutation-free + */ + public function values(): MutableVector + { + return MutableVector::fromArray($this->elements); + } + + /** + * As {@see MutableSet} does not have keys, this method acts as an alias for {@see MutableSet::values()}. + * + * @return MutableVector + * + * @psalm-mutation-free + */ + public function keys(): MutableVector + { + return MutableVector::fromArray($this->elements); + } + + /** + * Returns a `MutableSet` containing the values of the current `MutableSet` + * that meet a supplied condition. + * + * Only values that meet a certain criteria are affected by a call to + * `filter()`, while all values are affected by a call to `map()`. + * + * The keys associated with the current `MutableSet` remain unchanged in the + * returned `MutableSet`. + * + * @param (Closure(T): bool) $fn The callback containing the condition to apply to the current + * `MutableSet` values. + * + * @return MutableSet A `MutableSet` containing the values after a user-specified condition + * is applied. + */ + public function filter(Closure $fn): MutableSet + { + return new MutableSet(Dict\filter_keys($this->elements, $fn)); + } + + /** + * Applies a user-defined condition to each value in the `MutableSet`, + * considering the value as both key and value. + * + * This method extends {@see MutableSet::filter()} by providing the value twice to the + * callback function: once as the "key" and once as the "value", even though {@see MutableSet} do not have traditional key-value pairs. + * + * This allows for filtering based on both the value's "key" and "value" representation, which are identical. + * It's particularly useful when the distinction between keys and values is relevant for the condition. + * + * @param (Closure(T, T): bool) $fn T + * + * @return MutableSet + */ + public function filterWithKey(Closure $fn): MutableSet + { + return $this->filter(static fn($k) => $fn($k, $k)); + } + + /** + * Returns a `MutableSet` after an operation has been applied to each value + * in the current `MutableSet`. + * + * Every value in the current Map is affected by a call to `map()`, unlike + * `filter()` where only values that meet a certain criteria are affected. + * + * The keys will remain unchanged from the current `MutableSet` to the + * returned `MutableSet`. + * + * @template Tu of array-key + * + * @param (Closure(T): Tu) $fn The callback containing the operation to apply to the current + * `MutableSet` values. + * + * @return MutableSet A `MutableSet` containing the values after a user-specified + * operation is applied. + */ + public function map(Closure $fn): MutableSet + { + return new MutableSet(Dict\map($this->elements, $fn)); + } + + /** + * Transform the values of the current `MutableSet` by applying the provided callback, + * considering the value as both key and value. + * + * Similar to {@see MutableSet::map()}, this method extends the functionality by providing the value twice to the + * callback function: once as the "key" and once as the "value", + * + * The allows for transformations that take into account the value's dual role. It's useful for operations where the distinction + * between keys and values is relevant. + * + * @template Tu of array-key + * + * @param (Closure(T, T): Tu) $fn + * + * @return MutableSet + */ + public function mapWithKey(Closure $fn): MutableSet + { + return $this->map(static fn($k) => $fn($k, $k)); + } + + /** + * Always throws an exception since `MutableSet` can only contain array-key values. + * + * @template Tu + * + * @param array $elements The elements to use to combine with the elements of this `MutableSet`. + * + * @psalm-mutation-free + * + * @throws Exception\RuntimeException Always throws an exception since `MutableSet` can only contain array-key values. + */ + public function zip(array $elements): never + { + throw new Exception\RuntimeException('Cannot zip a MutableSet.'); + } + + /** + * Returns a `MutableSet` containing the first `n` values of the current + * `MutableSet`. + * + * The returned `MutableSet` will always be a proper subset of the current + * `MutableSet`. + * + * `$n` is 1-based. So the first element is 1, the second 2, etc. + * + * @param int<0, max> $n The last element that will be included in the returned + * `MutableSet`. + * + * @return MutableSet A `MutableSet` that is a proper subset of the current + * `MutableSet` up to `n` elements. + * + * @psalm-mutation-free + */ + public function take(int $n): MutableSet + { + return $this->slice(0, $n); + } + + /** + * Returns a `MutableSet` containing the values of the current `MutableSet` + * up to but not including the first value that produces `false` when passed + * to the specified callback. + * + * The returned `MutableSet` will always be a proper subset of the current + * `MutableSet`. + * + * @param (Closure(T): bool) $fn The callback that is used to determine the stopping + * condition. + * + * @return MutableSet A `MutableSet` that is a proper subset of the current + * `MutableSet` up until the callback returns `false`. + */ + public function takeWhile(Closure $fn): MutableSet + { + return new MutableSet(Dict\take_while($this->elements, $fn)); + } + + /** + * Returns a `MutableSet` containing the values after the `n`-th element of + * the current `MutableSet`. + * + * The returned `MutableSet` will always be a proper subset of the current + * `setInterface`. + * + * `$n` is 1-based. So the first element is 1, the second 2, etc. + * + * @param int<0, max> $n The last element to be skipped; the $n+1 element will be the + * first one in the returned `MutableSet`. + * + * @return MutableSet A `MutableSet` that is a proper subset of the current + * `MutableSet` containing values after the specified `n`-th element. + * + * @psalm-mutation-free + */ + public function drop(int $n): MutableSet + { + return $this->slice($n); + } + + /** + * Returns a `MutableSet` containing the values of the current `MutableSet` + * starting after and including the first value that produces `true` when + * passed to the specified callback. + * + * The returned `MutableSet` will always be a proper subset of the current + * `MutableSet`. + * + * @param (Closure(T): bool) $fn The callback used to determine the starting element for the + * returned `MutableSet`. + * + * @return MutableSet A `MutableSet` that is a proper subset of the current + * `MutableSet` starting after the callback returns `true`. + */ + public function dropWhile(Closure $fn): MutableSet + { + return new MutableSet(Dict\drop_while($this->elements, $fn)); + } + + /** + * Returns a subset of the current `MutableSet` starting from a given index up + * to, but not including, the element at the provided length from the starting + * index. + * + * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the + * elements at index 0 and 1. + * + * The returned `MutableSet` will always be a proper subset of this + * `MutableSet`. + * + * @param int<0, max> $start The starting index of this set to begin the returned + * `MutableSet`. + * @param int<0, max> $length The length of the returned `MutableSet`. + * + * @return MutableSet A `MutableSet` that is a proper subset of the current + * `MutableSet` starting at `$start` up to but not including + * the element `$start + $length`. + * + * @psalm-mutation-free + */ + public function slice(int $start, ?int $length = null): MutableSet + { + /** @psalm-suppress ImpureFunctionCall - conditionally pure */ + return MutableSet::fromArray(Dict\slice($this->elements, $start, $length)); + } + + /** + * Returns a `MutableVector` containing the original `MutableSet` split into + * chunks of the given size. + * + * If the original `MutableSet` doesn't divide evenly, the final chunk will be + * smaller. + * + * @param positive-int $size The size of each chunk. + * + * @return MutableVector> A `MutableVector` containing the original + * `MutableSet` split into chunks of the given size. + * + * @psalm-mutation-free + */ + public function chunk(int $size): MutableVector + { + /** + * @psalm-suppress MissingThrowsDocblock + * @psalm-suppress ImpureFunctionCall + */ + return MutableVector::fromArray(Vec\map( + /** + * @psalm-suppress MissingThrowsDocblock + * @psalm-suppress ImpureFunctionCall + */ + Vec\chunk($this->toArray(), $size), + /** + * @param list $chunk + * + * @return MutableSet + */ + static fn(array $chunk) => MutableSet::fromArray($chunk) + )); + } + + /** + * Determines if the specified offset exists in the current set. + * + * @param mixed $offset An offset to check for. + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + * + * @return bool Returns true if the specified offset exists, false otherwise. + * + * @psalm-mutation-free + * + * @psalm-assert array-key $offset + */ + public function offsetExists(mixed $offset): bool + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\InvalidOffsetException('Invalid set read offset type, expected a string or an integer.'); + } + + /** @var T $offset - technically, we don't know if the offset is of type T, but we can assume it is, as this causes no "harm". */ + return $this->contains($offset); + } + + /** + * Returns the value at the specified offset. + * + * @param mixed $offset The offset to retrieve. + * + * @throws Exception\InvalidOffsetException If the offset type is not array-key. + * @throws Exception\OutOfBoundsException If the offset does not exist. + * + * @return T The value at the specified offset. + * + * @psalm-mutation-free + * + * @psalm-assert array-key $offset + */ + public function offsetGet(mixed $offset): mixed + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\InvalidOffsetException('Invalid set read offset type, expected a string or an integer.'); + } + + /** @var T $offset - technically, we don't know if the offset is of type T, but we can assume it is, as this causes no "harm". */ + return $this->at($offset); + } + + /** + * Sets the value at the specified offset. + * + * @param mixed $offset The offset to assign the value to. + * @param T $value The value to set. + * + * @psalm-external-mutation-free + * + * @psalm-assert null|array-key $offset + * + * @throws Exception\InvalidOffsetException If the offset is not null or the value is not the same as the offset. + */ + public function offsetSet(mixed $offset, mixed $value): void + { + if (null === $offset || $offset === $value) { + $this->add($value); + + return; + } + + throw new Exception\InvalidOffsetException('Invalid set write offset type, expected null or the same as the value.'); + } + + /** + * Unsets the value at the specified offset. + * + * @param mixed $offset The offset to unset. + * + * @psalm-external-mutation-free + * + * @psalm-assert array-key $offset + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + */ + public function offsetUnset(mixed $offset): void + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\InvalidOffsetException('Invalid set read offset type, expected a string or an integer.'); + } + + /** @var T $offset - technically, we don't know if the offset is of type T, but we can assume it is, as this causes no "harm". */ + $this->remove($offset); + } +} diff --git a/src/Psl/Collection/MutableSetInterface.php b/src/Psl/Collection/MutableSetInterface.php new file mode 100644 index 00000000..c6b68e0c --- /dev/null +++ b/src/Psl/Collection/MutableSetInterface.php @@ -0,0 +1,374 @@ + + * @extends MutableAccessibleCollectionInterface + */ +interface MutableSetInterface extends MutableAccessibleCollectionInterface, SetInterface +{ + /** + * Returns the provided value if it exists in the current `MutableSetInterface`. + * + * As {@see MutableSetInterface} does not have keys, this method checks if the value exists in the set. + * If the value exists, it is returned to indicate presence in the set. If the value does not exist, + * an {@see Exception\OutOfBoundsException} is thrown to indicate the absence of the value. + * + * @param T $k + * + * @throws Exception\OutOfBoundsException If $k is out-of-bounds. + * + * @return T + * + * @psalm-mutation-free + */ + public function at(int|string $k): int|string; + + /** + * Determines if the specified value is in the current set. + * + * As {@see MutableSetInterface} does not have keys, this method checks if the value exists in the set. + * If the value exists, it returns true to indicate presence in the set. If the value does not exist, + * it returns false to indicate the absence of the value. + * + * @param T $k + * + * @return bool True if the value is in the set, false otherwise. + * + * @psalm-mutation-free + */ + public function contains(int|string $k): bool; + + /** + * Returns the provided value if it is part of the set, or null if it is not. + * + * As {@see MutableSetInterface} does not have keys, this method checks if the value exists in the set. + * If the value exists, it is returned to indicate presence in the set. If the value does not exist, + * null is returned to indicate the absence of the value. + * + * @param T $k + * + * @return T|null + * + * @psalm-mutation-free + */ + public function get(int|string $k): null|int|string; + + /** + * Get an array copy of the current set. + * + * @return array + * + * @psalm-mutation-free + */ + public function toArray(): array; + + /** + * Returns a `MutableVectorInterface` containing the values of the current `MutableSetInterface`. + * + * @return MutableVectorInterface + * + * @psalm-mutation-free + */ + public function values(): MutableVectorInterface; + + /** + * As {@see MutableSetInterface} does not have keys, this method acts as an alias for {@see MutableSetInterface::values()}. + * + * @return MutableVectorInterface + * + * @psalm-mutation-free + */ + public function keys(): MutableVectorInterface; + + /** + * Returns a `MutableSetInterface` containing the values of the current `MutableSetInterface` + * that meet a supplied condition. + * + * Only values that meet a certain criteria are affected by a call to + * `filter()`, while all values are affected by a call to `map()`. + * + * @param (Closure(T): bool) $fn The callback containing the condition to apply to the current + * `MutableSetInterface` values + * + * @return MutableSetInterface A MutableSetInterface containing the values after + * a user-specified condition is applied. + */ + public function filter(Closure $fn): MutableSetInterface; + + /** + * Applies a user-defined condition to each value in the `MutableSetInterface`, + * considering the value as both key and value. + * + * This method extends {@see MutableSetInterface::filter()} by providing the value twice to the + * callback function: once as the "key" and once as the "value", even though {@see MutableSetInterface} do not have traditional key-value pairs. + * + * This allows for filtering based on both the value's "key" and "value" representation, which are identical. + * It's particularly useful when the distinction between keys and values is relevant for the condition. + * + * @param (Closure(T, T): bool) $fn T + * + * @return MutableSetInterface + */ + public function filterWithKey(Closure $fn): MutableSetInterface; + + /** + * Returns a `MutableSetInterface` after an operation has been applied to each value + * in the current `MutableSetInterface`. + * + * Every value in the current Map is affected by a call to `map()`, unlike + * `filter()` where only values that meet a certain criteria are affected. + * + * @template Tu of array-key + * + * @param (Closure(T): Tu) $fn The callback containing the operation to apply to the current + * `MutableSetInterface` values + * + * @return MutableSetInterface A `MutableSetInterface` containing the values after a user-specified + * operation is applied. + */ + public function map(Closure $fn): MutableSetInterface; + + /** + * Transform the values of the current `MutableSetInterface` by applying the provided callback, + * considering the value as both key and value. + * + * Similar to {@see MutableSetInterface::map()}, this method extends the functionality by providing the value twice to the + * callback function: once as the "key" and once as the "value", + * + * The allows for transformations that take into account the value's dual role. It's useful for operations where the distinction + * between keys and values is relevant. + * + * @template Tu of array-key + * + * @param (Closure(T, T): Tu) $fn + * + * @return MutableSetInterface + */ + public function mapWithKey(Closure $fn): MutableSetInterface; + + /** + * Returns the first value in the current `MutableSetInterface`. + * + * @return T|null The first value in the current `MutableSetInterface`, or `null` if the + * current `MutableSetInterface` is empty. + * + * @psalm-mutation-free + */ + public function first(): null|int|string; + + /** + * Returns the first key in the current `MutableSetInterface`. + * + * As {@see MutableSetInterface} does not have keys, this method acts as an alias for {@see MutableSetInterface::first()}. + * + * @return T|null The first value in the current `MutableSetInterface`, or `null` if the + * current `MutableSetInterface` is empty. + * + * @psalm-mutation-free + */ + public function firstKey(): null|int|string; + + /** + * Returns the last value in the current `MutableSetInterface`. + * + * @return T|null The last value in the current `MutableSetInterface`, or `null` if the + * current `MutableSetInterface` is empty. + * + * @psalm-mutation-free + */ + public function last(): null|int|string; + + /** + * Returns the last key in the current `MutableSetInterface`. + * + * As {@see MutableSetInterface} does not have keys, this method acts as an alias for {@see MutableSetInterface::last()}. + * + * @return T|null The last value in the current `MutableSetInterface`, or `null` if the + * current `MutableSetInterface` is empty. + * + * @psalm-mutation-free + */ + public function lastKey(): null|int|string; + + /** + * Returns the key of the first element that matches the search value. + * + * If no element matches the search value, this function returns null. + * + * As {@see MutableSetInterface} does not have keys, this method returns the value itself. + * + * @param T $search_value The value that will be search for in the current + * `MutableSetInterface`. + * + * @return T|null The value if its found, null otherwise. + * + * @psalm-mutation-free + */ + public function linearSearch(mixed $search_value): null|int|string; + + /** + * Always throws an exception since `Set` can only contain array-key values. + * + * @template Tu + * + * @param array $elements The elements to use to combine with the elements of this `SetInterface`. + * + * @psalm-mutation-free + * + * @throws Exception\RuntimeException Always throws an exception since `Set` can only contain array-key values. + */ + public function zip(array $elements): never; + + /** + * Returns a `MutableSetInterface` containing the first `n` values of the current + * `MutableSetInterface`. + * + * The returned `MutableSetInterface` will always be a proper subset of the current + * `MutableSetInterface`. + * + * `$n` is 1-based. So the first element is 1, the second 2, etc. + * + * @param int<0, max> $n The last element that will be included in the returned + * `MutableSetInterface`. + * + * @return MutableSetInterface A `MutableSetInterface` that is a proper subset of the current + * `MutableSetInterface` up to `n` elements. + * + * @psalm-mutation-free + */ + public function take(int $n): MutableSetInterface; + + /** + * Returns a `MutableSetInterface` containing the values of the current `MutableSetInterface` + * up to but not including the first value that produces `false` when passed + * to the specified callback. + * + * The returned `MutableSetInterface` will always be a proper subset of the current + * `MutableSetInterface`. + * + * @param (Closure(T): bool) $fn The callback that is used to determine the stopping + * condition. + * + * @return MutableSetInterface A `MutableSetInterface` that is a proper subset of the current + * `MutableSetInterface` up until the callback returns `false`. + */ + public function takeWhile(Closure $fn): MutableSetInterface; + + /** + * Returns a `MutableSetInterface` containing the values after the `n`-th element of + * the current `MutableSetInterface`. + * + * The returned `MutableSetInterface` will always be a proper subset of the current + * `MutableSetInterface`. + * + * `$n` is 1-based. So the first element is 1, the second 2, etc. + * + * @param int<0, max> $n The last element to be skipped; the $n+1 element will be the + * first one in the returned `MutableSetInterface`. + * + * @return MutableSetInterface A `MutableSetInterface` that is a proper subset of the current + * `MutableSetInterface` containing values after the specified `n`-th element. + * + * @psalm-mutation-free + */ + public function drop(int $n): MutableSetInterface; + + /** + * Returns a `MutableSetInterface` containing the values of the current `MutableSetInterface` + * starting after and including the first value that produces `true` when + * passed to the specified callback. + * + * The returned `MutableSetInterface` will always be a proper subset of the current + * `MutableSetInterface`. + * + * @param (Closure(T): bool) $fn The callback used to determine the starting element for the + * returned `MutableSetInterface`. + * + * @return MutableSetInterface A `MutableSetInterface` that is a proper subset of the current + * `MutableSetInterface` starting after the callback returns `true`. + */ + public function dropWhile(Closure $fn): MutableSetInterface; + + /** + * Returns a subset of the current `MutableSetInterface` starting from a given index up + * to, but not including, the element at the provided length from the starting + * index. + * + * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the + * elements at index 0 and 1. + * + * The returned `MutableSetInterface` will always be a proper subset of this + * `MutableSetInterface`. + * + * @param int<0, max> $start The starting index of this set to begin the returned + * `MutableSetInterface`. + * @param int<0, max> $length The length of the returned `MutableSetInterface`. + * + * @return MutableSetInterface A `MutableSetInterface` that is a proper subset of the current + * `MutableSetInterface` starting at `$start` up to but not including + * the element `$start + $length`. + * + * @psalm-mutation-free + */ + public function slice(int $start, ?int $length = null): MutableSetInterface; + + /** + * Returns a `MutableVectorInterface` containing the original `MutableSetInterface` split into + * chunks of the given size. + * + * If the original `MutableSetInterface` doesn't divide evenly, the final chunk will be + * smaller. + * + * @param positive-int $size The size of each chunk. + * + * @return MutableVectorInterface> A `MutableVectorInterface` containing the original + * `MutableSetInterface` split into chunks of the given size. + * + * @psalm-mutation-free + */ + public function chunk(int $size): MutableVectorInterface; + + /** + * Removes the specified value from the current set. + * + * If the value is not in the current set, the current set is unchanged. + * + * @param T $k The value to remove. + * + * @return MutableSetInterface Returns itself. + */ + public function remove(int|string $k): MutableSetInterface; + + /** + * Removes all elements from the set. + * + * @return MutableSetInterface + */ + public function clear(): MutableSetInterface; + + /** + * Add a value to the set and return the set itself. + * + * @param T $v The value to add. + * + * @return MutableSetInterface Returns itself. + */ + public function add(mixed $v): MutableSetInterface; + + /** + * For every element in the provided elements iterable, add the value into the current set. + * + * @param iterable $elements The elements with the new values to add. + * + * @return MutableSetInterface Returns itself. + */ + public function addAll(iterable $elements): MutableSetInterface; +} diff --git a/src/Psl/Collection/MutableVector.php b/src/Psl/Collection/MutableVector.php index fe0e3d2b..f6d7f360 100644 --- a/src/Psl/Collection/MutableVector.php +++ b/src/Psl/Collection/MutableVector.php @@ -168,7 +168,7 @@ public function jsonSerialize(): array * * @psalm-mutation-free */ - public function at(string|int $k): mixed + public function at(int|string $k): mixed { if (!array_key_exists($k, $this->elements)) { throw Exception\OutOfBoundsException::for($k); @@ -198,7 +198,7 @@ public function contains(int|string $k): bool * * @psalm-mutation-free */ - public function get(string|int $k): mixed + public function get(int|string $k): mixed { return $this->elements[$k] ?? null; } @@ -369,15 +369,15 @@ public function add(mixed $v): MutableVector } /** - * For every element in the provided elements array, add the value into the current vector. + * For every element in the provided elements iterable, add the value into the current vector. * - * @param array $elements The elements with the new values to add + * @param iterable $elements The elements with the new values to add * * @return MutableVector returns itself. * * @psalm-external-mutation-free */ - public function addAll(array $elements): MutableVector + public function addAll(iterable $elements): MutableVector { foreach ($elements as $item) { $this->add($item); @@ -670,4 +670,98 @@ public function chunk(int $size): MutableVector static fn(array $chunk) => MutableVector::fromArray($chunk) )); } + + + /** + * Determines if the specified offset exists in the current vector. + * + * @param mixed $offset An offset to check for. + * + * @throws Exception\InvalidOffsetException If the offset type is not a positive integer. + * + * @return bool Returns true if the specified offset exists, false otherwise. + * + * @psalm-mutation-free + * + * @psalm-assert int<0, max> $offset + */ + public function offsetExists(mixed $offset): bool + { + if (!is_int($offset) || $offset < 0) { + throw new Exception\InvalidOffsetException('Invalid vector read offset type, expected a positive integer.'); + } + + return $this->contains($offset); + } + + /** + * Returns the value at the specified offset. + * + * @param mixed $offset The offset to retrieve. + * + * @throws Exception\InvalidOffsetException If the offset type is not a positive integer. + * @throws Exception\OutOfBoundsException If the offset does not exist. + * + * @return T|null The value at the specified offset, null if the offset does not exist. + * + * @psalm-mutation-free + * + * @psalm-assert int<0, max> $offset + */ + public function offsetGet(mixed $offset): mixed + { + if (!is_int($offset) || $offset < 0) { + throw new Exception\InvalidOffsetException('Invalid vector read offset type, expected a positive integer.'); + } + + return $this->at($offset); + } + + /** + * Sets the value at the specified offset. + * + * @param mixed $offset The offset to assign the value to. + * @param T $value The value to set. + * + * @psalm-external-mutation-free + * + * @psalm-assert null|int<0, max> $offset + * + * @throws Exception\InvalidOffsetException If the offset is not null or a positive integer. + * @throws Exception\OutOfBoundsException If the offset is out-of-bounds. + */ + public function offsetSet(mixed $offset, mixed $value): void + { + if (null === $offset) { + $this->add($value); + + return; + } + + if (!is_int($offset) || $offset < 0) { + throw new Exception\InvalidOffsetException('Invalid vector write offset type, expected a positive integer or null.'); + } + + $this->set($offset, $value); + } + + /** + * Unsets the value at the specified offset. + * + * @param mixed $offset The offset to unset. + * + * @psalm-external-mutation-free + * + * @psalm-assert array-key $offset + * + * @throws Exception\InvalidOffsetException If the offset type is not valid. + */ + public function offsetUnset(mixed $offset): void + { + if (!is_int($offset) || $offset < 0) { + throw new Exception\InvalidOffsetException('Invalid vector read offset type, expected a positive integer.'); + } + + $this->remove($offset); + } } diff --git a/src/Psl/Collection/MutableVectorInterface.php b/src/Psl/Collection/MutableVectorInterface.php index 50cb57e6..3a507f97 100644 --- a/src/Psl/Collection/MutableVectorInterface.php +++ b/src/Psl/Collection/MutableVectorInterface.php @@ -379,9 +379,9 @@ public function add(mixed $v): MutableVectorInterface; /** * For every element in the provided elements array, add the value into the current vector. * - * @param array $elements The elements with the new values to add. + * @param iterable $elements The elements with the new values to add. * * @return MutableVectorInterface Returns itself. */ - public function addAll(array $elements): MutableVectorInterface; + public function addAll(iterable $elements): MutableVectorInterface; } diff --git a/src/Psl/Collection/Set.php b/src/Psl/Collection/Set.php new file mode 100644 index 00000000..54a1e276 --- /dev/null +++ b/src/Psl/Collection/Set.php @@ -0,0 +1,553 @@ + + */ +final readonly class Set implements SetInterface +{ + /** + * @var array $elements + */ + private array $elements; + + /** + * Creates a new `Set` containing the values of the given array. + * + * @param array $elements + * + * @psalm-mutation-free + */ + public function __construct(array $elements) + { + $set = []; + foreach ($elements as $element) { + $set[$element] = $element; + } + + $this->elements = $set; + } + + /** + * Creates and returns a default instance of {@see Set}. + * + * @return static A default instance of {@see Set}. + * + * @pure + */ + public static function default(): static + { + return new self([]); + } + + /** + * Create a set from the given array, using the values of the array as the set values. + * + * @template Ts of array-key + * + * @param array $elements + * + * @return Set + * + * @pure + */ + public static function fromArray(array $elements): Set + { + return new self($elements); + } + + /** + * Create a set from the given $elements array, using the keys of the array as the set values. + * + * @template Ts of array-key + * + * @param array $elements + * + * @return Set + * + * @pure + */ + public static function fromArrayKeys(array $elements): Set + { + /** @var array $set */ + $set = []; + foreach ($elements as $key => $_) { + $set[$key] = $key; + } + + return new self($set); + } + + /** + * Returns the first value in the current `Set`. + * + * @return T|null The first value in the current `Set`, or `null` if the + * current `Set` is empty. + * + * @psalm-mutation-free + */ + public function first(): null|int|string + { + return array_key_first($this->elements); + } + + /** + * Returns the last value in the current `Set`. + * + * @return T|null The last value in the current `Set`, or `null` if the + * current `Set` is empty. + * + * @psalm-mutation-free + */ + public function last(): null|int|string + { + return array_key_last($this->elements); + } + + /** + * Retrieve an external iterator. + * + * @return Iter\Iterator + */ + public function getIterator(): Iter\Iterator + { + return Iter\Iterator::create($this->elements); + } + + /** + * Is the `Set` empty? + * + * @psalm-mutation-free + */ + public function isEmpty(): bool + { + return [] === $this->elements; + } + + /** + * Get the number of elements in the current `Set`. + * + * @psalm-mutation-free + * + * @return int<0, max> + */ + public function count(): int + { + /** @var int<0, max> */ + return count($this->elements); + } + + /** + * Get an array copy of the current `Set`. + * + * @return array + * + * @psalm-mutation-free + */ + public function toArray(): array + { + return $this->elements; + } + + + /** + * Get an array copy of the current `Set`. + * + * @return array + * + * @psalm-mutation-free + */ + public function jsonSerialize(): array + { + return $this->elements; + } + + /** + * Returns the provided value if it exists in the current `Set`. + * + * As {@see Set} does not have keys, this method checks if the value exists in the set. + * If the value exists, it is returned to indicate presence in the set. If the value does not exist, + * an {@see Exception\OutOfBoundsException} is thrown to indicate the absence of the value. + * + * @param T $k + * + * @throws Exception\OutOfBoundsException If $k is out-of-bounds. + * + * @return T + * + * @psalm-mutation-free + */ + public function at(int|string $k): int|string + { + if (!array_key_exists($k, $this->elements)) { + throw Exception\OutOfBoundsException::for($k); + } + + return $this->elements[$k]; + } + + /** + * Determines if the specified value is in the current set. + * + * As {@see Set} does not have keys, this method checks if the value exists in the set. + * If the value exists, it returns true to indicate presence in the set. If the value does not exist, + * it returns false to indicate the absence of the value. + * + * @param T $k + * + * @return bool True if the value is in the set, false otherwise. + * + * @psalm-mutation-free + */ + public function contains(int|string $k): bool + { + return array_key_exists($k, $this->elements); + } + + /** + * Returns the provided value if it is part of the set, or null if it is not. + * + * As {@see Set} does not have keys, this method checks if the value exists in the set. + * If the value exists, it is returned to indicate presence in the set. If the value does not exist, + * null is returned to indicate the absence of the value. + * + * @param T $k + * + * @return T|null + * + * @psalm-mutation-free + */ + public function get(int|string $k): null|int|string + { + return $this->elements[$k] ?? null; + } + + /** + * Returns the first key in the current `Set`. + * + * As {@see Set} does not have keys, this method acts as an alias for {@see Set::first()}. + * + * @return T|null The first value in the current `Set`, or `null` if the + * current `Set` is empty. + * + * @psalm-mutation-free + */ + public function firstKey(): null|int|string + { + return $this->first(); + } + + /** + * Returns the last key in the current `Set`. + * + * As {@see Set} does not have keys, this method acts as an alias for {@see Set::last()}. + * + * @return T|null The last value in the current `Set`, or `null` if the + * current `Set` is empty. + * + * @psalm-mutation-free + */ + public function lastKey(): null|int|string + { + return $this->last(); + } + + /** + * Returns the key of the first element that matches the search value. + * + * If no element matches the search value, this function returns null. + * + * As {@see Set} does not have keys, this method returns the value itself. + * + * @param T $search_value The value that will be search for in the current `Set`. + * + * @return T|null The value if its found, null otherwise. + * + * @psalm-mutation-free + */ + public function linearSearch(mixed $search_value): null|int|string + { + foreach ($this->elements as $key => $element) { + if ($search_value === $element) { + return $key; + } + } + + return null; + } + + /** + * Returns a `Vector` containing the values of the current `Set`. + * + * @return Vector + * + * @psalm-mutation-free + */ + public function values(): Vector + { + return Vector::fromArray($this->elements); + } + + /** + * As {@see Set} does not have keys, this method acts as an alias for {@see Set::values()}. + * + * @return Vector + * + * @psalm-mutation-free + */ + public function keys(): Vector + { + return Vector::fromArray(array_keys($this->elements)); + } + + /** + * Returns a `Set` containing the values of the current `Set` + * that meet a supplied condition. + * + * Only values that meet a certain criteria are affected by a call to + * `filter()`, while all values are affected by a call to `map()`. + * + * @param (Closure(T): bool) $fn The callback containing the condition to apply to the current + * `Set` values. + * + * @return Set a Set containing the values after a user-specified condition + * is applied. + */ + public function filter(Closure $fn): Set + { + return new Set(Dict\filter_keys($this->elements, $fn)); + } + + + /** + * Applies a user-defined condition to each value in the `Set`, + * considering the value as both key and value. + * + * This method extends {@see Set::filter()} by providing the value twice to the + * callback function: once as the "key" and once as the "value", even though {@see Set} do not have traditional key-value pairs. + * + * This allows for filtering based on both the value's "key" and "value" representation, which are identical. + * It's particularly useful when the distinction between keys and values is relevant for the condition. + * + * @param (Closure(T, T): bool) $fn T + * + * @return Set + */ + public function filterWithKey(Closure $fn): Set + { + return $this->filter(static fn($k) => $fn($k, $k)); + } + + /** + * Returns a `Set` after an operation has been applied to each value + * in the current `Set`. + * + * Every value in the current Map is affected by a call to `map()`, unlike + * `filter()` where only values that meet a certain criteria are affected. + * + * @template Tu of array-key + * + * @param (Closure(T): Tu) $fn The callback containing the operation to apply to the current + * `Set` values. + * + * @return Set a `Set` containing key/value pairs after a user-specified + * operation is applied. + */ + public function map(Closure $fn): Set + { + return new Set(Dict\map($this->elements, $fn)); + } + + /** + * Transform the values of the current `Set` by applying the provided callback, + * considering the value as both key and value. + * + * Similar to {@see Set::map()}, this method extends the functionality by providing the value twice to the + * callback function: once as the "key" and once as the "value", + * + * The allows for transformations that take into account the value's dual role. It's useful for operations where the distinction + * between keys and values is relevant. + * + * @template Tu of array-key + * + * @param (Closure(T, T): Tu) $fn + * + * @return Set + */ + public function mapWithKey(Closure $fn): Set + { + return $this->map(static fn($k) => $fn($k, $k)); + } + + /** + * Always throws an exception since `Set` can only contain array-key values. + * + * @template Tu + * + * @param array $elements The elements to use to combine with the elements of this `SetInterface`. + * + * @psalm-mutation-free + * + * @throws Exception\RuntimeException Always throws an exception since `Set` can only contain array-key values. + */ + public function zip(array $elements): never + { + throw new Exception\RuntimeException('Cannot zip a Set.'); + } + + /** + * Returns a `Set` containing the first `n` values of the current + * `Set`. + * + * The returned `Set` will always be a proper subset of the current + * `Set`. + * + * `$n` is 1-based. So the first element is 1, the second 2, etc. + * + * @param int<0, max> $n The last element that will be included in the returned + * `Set`. + * + * @return Set A `Set` that is a proper subset of the current + * `Set` up to `n` elements. + * + * @psalm-mutation-free + */ + public function take(int $n): Set + { + return $this->slice(0, $n); + } + + /** + * Returns a `Set` containing the values of the current `Set` + * up to but not including the first value that produces `false` when passed + * to the specified callback. + * + * The returned `Set` will always be a proper subset of the current + * `Set`. + * + * @param (Closure(T): bool) $fn The callback that is used to determine the stopping + * condition. + * + * @return Set A `Set` that is a proper subset of the current + * `Set` up until the callback returns `false`. + */ + public function takeWhile(Closure $fn): Set + { + return new Set(Dict\take_while($this->elements, $fn)); + } + + /** + * Returns a `Set` containing the values after the `n`-th element of + * the current `Set`. + * + * The returned `Set` will always be a proper subset of the current + * `SetInterface`. + * + * `$n` is 1-based. So the first element is 1, the second 2, etc. + * + * @param int<0, max> $n The last element to be skipped; the $n+1 element will be the + * first one in the returned `Set`. + * + * @return Set A `Set` that is a proper subset of the current + * `Set` containing values after the specified `n`-th element. + * + * @psalm-mutation-free + */ + public function drop(int $n): Set + { + return $this->slice($n); + } + + /** + * Returns a `Set` containing the values of the current `Set` + * starting after and including the first value that produces `true` when + * passed to the specified callback. + * + * The returned `Set` will always be a proper subset of the current + * `Set`. + * + * @param (Closure(T): bool) $fn The callback used to determine the starting element for the + * returned `Set`. + * + * @return Set A `Set` that is a proper subset of the current + * `Set` starting after the callback returns `true`. + */ + public function dropWhile(Closure $fn): Set + { + return new Set(Dict\drop_while($this->elements, $fn)); + } + + /** + * Returns a subset of the current `Set` starting from a given index up + * to, but not including, the element at the provided length from the starting + * index. + * + * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the + * elements at index 0 and 1. + * + * The returned `Set` will always be a proper subset of this + * `Set`. + * + * @param int<0, max> $start The starting index of this set to begin the returned + * `Set`. + * @param int<0, max> $length The length of the returned `Set`. + * + * @return Set A `Set` that is a proper subset of the current + * `Set` starting at `$start` up to but not including + * the element `$start + $length`. + * + * @psalm-mutation-free + */ + public function slice(int $start, ?int $length = null): Set + { + /** @psalm-suppress ImpureFunctionCall - conditionally pure */ + return self::fromArray(Dict\slice($this->elements, $start, $length)); + } + + /** + * Returns a `Vector` containing the original `Set` split into + * chunks of the given size. + * + * If the original `Set` doesn't divide evenly, the final chunk will be + * smaller. + * + * @param positive-int $size The size of each chunk. + * + * @return Vector> A `Vector` containing the original `Set` split + * into chunks of the given size. + * + * @psalm-mutation-free + */ + public function chunk(int $size): Vector + { + /** + * @psalm-suppress MissingThrowsDocblock + * @psalm-suppress ImpureFunctionCall + */ + return Vector::fromArray(Vec\map( + Vec\chunk($this->toArray(), $size), + /** + * @param list $chunk + * + * @return Set + */ + static fn(array $chunk) => static::fromArray($chunk) + )); + } +} diff --git a/src/Psl/Collection/SetInterface.php b/src/Psl/Collection/SetInterface.php new file mode 100644 index 00000000..e8ba3fba --- /dev/null +++ b/src/Psl/Collection/SetInterface.php @@ -0,0 +1,337 @@ + + */ +interface SetInterface extends AccessibleCollectionInterface +{ + /** + * Returns the provided value if it exists in the current `SetInterface`. + * + * As {@see SetInterface} does not have keys, this method checks if the value exists in the set. + * If the value exists, it is returned to indicate presence in the set. If the value does not exist, + * an {@see Exception\OutOfBoundsException} is thrown to indicate the absence of the value. + * + * @param T $k + * + * @throws Exception\OutOfBoundsException If $k is out-of-bounds. + * + * @return T + * + * @psalm-mutation-free + */ + public function at(int|string $k): int|string; + + /** + * Determines if the specified value is in the current set. + * + * As {@see SetInterface} does not have keys, this method checks if the value exists in the set. + * If the value exists, it returns true to indicate presence in the set. If the value does not exist, + * it returns false to indicate the absence of the value. + * + * @param T $k + * + * @return bool True if the value is in the set, false otherwise. + * + * @psalm-mutation-free + */ + public function contains(int|string $k): bool; + + /** + * Returns the provided value if it is part of the set, or null if it is not. + * + * As {@see SetInterface} does not have keys, this method checks if the value exists in the set. + * If the value exists, it is returned to indicate presence in the set. If the value does not exist, + * null is returned to indicate the absence of the value. + * + * @param T $k + * + * @return T|null + * + * @psalm-mutation-free + */ + public function get(int|string $k): null|int|string; + + /** + * Get an array copy of the current set. + * + * @return array + * + * @psalm-mutation-free + */ + public function toArray(): array; + + /** + * Returns a `VectorInterface` containing the values of the current `SetInterface`. + * + * @return VectorInterface + * + * @psalm-mutation-free + */ + public function values(): VectorInterface; + + /** + * As {@see SetInterface} does not have keys, this method acts as an alias for {@see SetInterface::values()}. + * + * @return VectorInterface + * + * @psalm-mutation-free + */ + public function keys(): VectorInterface; + + /** + * Returns a `SetInterface` containing the values of the current `SetInterface` + * that meet a supplied condition. + * + * Only values that meet a certain criteria are affected by a call to + * `filter()`, while all values are affected by a call to `map()`. + * + * @param (Closure(T): bool) $fn The callback containing the condition to apply to the current + * `SetInterface` values. + * + * @return SetInterface A SetInterface containing the values after a user-specified condition + * is applied. + */ + public function filter(Closure $fn): SetInterface; + + /** + * Applies a user-defined condition to each value in the `SetInterface`, + * considering the value as both key and value. + * + * This method extends {@see SetInterface::filter()} by providing the value twice to the + * callback function: once as the "key" and once as the "value", even though {@see SetInterface} do not have traditional key-value pairs. + * + * This allows for filtering based on both the value's "key" and "value" representation, which are identical. + * It's particularly useful when the distinction between keys and values is relevant for the condition. + * + * @param (Closure(T, T): bool) $fn T + * + * @return SetInterface + */ + public function filterWithKey(Closure $fn): SetInterface; + + /** + * Returns a `SetInterface` after an operation has been applied to each value + * in the current `SetInterface`. + * + * Every value in the current Map is affected by a call to `map()`, unlike + * `filter()` where only values that meet a certain criteria are affected. + * + * @template Tu of array-key + * + * @param (Closure(T): Tu) $fn The callback containing the operation to apply to the current + * `SetInterface` values. + * + * @return SetInterface A `SetInterface` containing key/value pairs after a user-specified + * operation is applied. + */ + public function map(Closure $fn): SetInterface; + + /** + * Transform the values of the current `SetInterface` by applying the provided callback, + * considering the value as both key and value. + * + * Similar to {@see SetInterface::map()}, this method extends the functionality by providing the value twice to the + * callback function: once as the "key" and once as the "value", + * + * The allows for transformations that take into account the value's dual role. It's useful for operations where the distinction + * between keys and values is relevant. + * + * @template Tu of array-key + * + * @param (Closure(T, T): Tu) $fn + * + * @return SetInterface + */ + public function mapWithKey(Closure $fn): SetInterface; + + /** + * Returns the first value in the current `SetInterface`. + * + * @return T|null The first value in the current `SetInterface`, or `null` if the + * current `SetInterface` is empty. + * + * @psalm-mutation-free + */ + public function first(): null|int|string; + + /** + * Returns the first key in the current `SetInterface`. + * + * As {@see SetInterface} does not have keys, this method acts as an alias for {@see SetInterface::first()}. + * + * @return T|null The first value in the current `SetInterface`, or `null` if the + * current `SetInterface` is empty. + * + * @psalm-mutation-free + */ + public function firstKey(): null|int|string; + + /** + * Returns the last value in the current `SetInterface`. + * + * @return T|null The last value in the current `SetInterface`, or `null` if the + * current `SetInterface` is empty. + * + * @psalm-mutation-free + */ + public function last(): null|int|string; + + /** + * Returns the last key in the current `SetInterface`. + * + * As {@see SetInterface} does not have keys, this method acts as an alias for {@see SetInterface::last()}. + * + * @return T|null The last value in the current `SetInterface`, or `null` if the + * current `SetInterface` is empty. + * + * @psalm-mutation-free + */ + public function lastKey(): null|int|string; + + /** + * Returns the key of the first element that matches the search value. + * + * If no element matches the search value, this function returns null. + * + * As {@see SetInterface} does not have keys, this method returns the value itself. + * + * @param T $search_value The value that will be search for in the current + * `SetInterface`. + * + * @return T|null The value if its found, null otherwise. + * + * @psalm-mutation-free + */ + public function linearSearch(mixed $search_value): null|int|string; + + /** + * Always throws an exception since `Set` can only contain array-key values. + * + * @template Tu + * + * @param array $elements The elements to use to combine with the elements of this `SetInterface`. + * + * @psalm-mutation-free + * + * @throws Exception\RuntimeException Always throws an exception since `Set` can only contain array-key values. + */ + public function zip(array $elements): never; + + /** + * Returns a `SetInterface` containing the first `n` values of the current + * `SetInterface`. + * + * The returned `SetInterface` will always be a proper subset of the current + * `SetInterface`. + * + * `$n` is 1-based. So the first element is 1, the second 2, etc. + * + * @param int<0, max> $n The last element that will be included in the returned + * `SetInterface`. + * + * @return SetInterface A `SetInterface` that is a proper subset of the current + * `SetInterface` up to `n` elements. + * + * @psalm-mutation-free + */ + public function take(int $n): SetInterface; + + /** + * Returns a `SetInterface` containing the values of the current `SetInterface` + * up to but not including the first value that produces `false` when passed + * to the specified callback. + * + * The returned `SetInterface` will always be a proper subset of the current + * `SetInterface`. + * + * @param (Closure(T): bool) $fn The callback that is used to determine the stopping + * condition. + * + * @return SetInterface A `SetInterface` that is a proper subset of the current + * `SetInterface` up until the callback returns `false`. + */ + public function takeWhile(Closure $fn): SetInterface; + + /** + * Returns a `SetInterface` containing the values after the `n`-th element of + * the current `SetInterface`. + * + * The returned `SetInterface` will always be a proper subset of the current + * `SetInterface`. + * + * `$n` is 1-based. So the first element is 1, the second 2, etc. + * + * @param int<0, max> $n The last element to be skipped; the $n+1 element will be the + * first one in the returned `SetInterface`. + * + * @return SetInterface A `SetInterface` that is a proper subset of the current + * `SetInterface` containing values after the specified `n`-th element. + * + * @psalm-mutation-free + */ + public function drop(int $n): SetInterface; + + /** + * Returns a `SetInterface` containing the values of the current `SetInterface` + * starting after and including the first value that produces `true` when + * passed to the specified callback. + * + * The returned `SetInterface` will always be a proper subset of the current + * `SetInterface`. + * + * @param (Closure(T): bool) $fn The callback used to determine the starting element for the + * returned `SetInterface`. + * + * @return SetInterface A `SetInterface` that is a proper subset of the current + * `SetInterface` starting after the callback returns `true`. + */ + public function dropWhile(Closure $fn): SetInterface; + + /** + * Returns a subset of the current `SetInterface` starting from a given index up + * to, but not including, the element at the provided length from the starting + * index. + * + * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the + * elements at index 0 and 1. + * + * The returned `SetInterface` will always be a proper subset of this + * `SetInterface`. + * + * @param int<0, max> $start The starting index of this set to begin the returned + * `SetInterface`. + * @param int<0, max> $length The length of the returned `SetInterface`. + * + * @return SetInterface A `SetInterface` that is a proper subset of the current + * `SetInterface` starting at `$start` up to but not including + * the element `$start + $length`. + * + * @psalm-mutation-free + */ + public function slice(int $start, ?int $length = null): SetInterface; + + /** + * Returns a `VectorInterface` containing the original `SetInterface` split into + * chunks of the given size. + * + * If the original `SetInterface` doesn't divide evenly, the final chunk will be + * smaller. + * + * @param positive-int $size The size of each chunk. + * + * @return VectorInterface> A `VectorInterface` containing the original + * `SetInterface` split into chunks of the given size. + * + * @psalm-mutation-free + */ + public function chunk(int $size): VectorInterface; +} diff --git a/src/Psl/Collection/Vector.php b/src/Psl/Collection/Vector.php index 56095ce6..2e34ef17 100644 --- a/src/Psl/Collection/Vector.php +++ b/src/Psl/Collection/Vector.php @@ -169,7 +169,7 @@ public function jsonSerialize(): array * * @psalm-mutation-free */ - public function at(string|int $k): mixed + public function at(int|string $k): mixed { if (!array_key_exists($k, $this->elements)) { throw Exception\OutOfBoundsException::for($k); @@ -199,7 +199,7 @@ public function contains(int|string $k): bool * * @psalm-mutation-free */ - public function get(string|int $k): mixed + public function get(int|string $k): mixed { return $this->elements[$k] ?? null; } diff --git a/src/Psl/Collection/VectorInterface.php b/src/Psl/Collection/VectorInterface.php index 01e41f28..284b42da 100644 --- a/src/Psl/Collection/VectorInterface.php +++ b/src/Psl/Collection/VectorInterface.php @@ -22,7 +22,7 @@ interface VectorInterface extends AccessibleCollectionInterface * * @psalm-mutation-free */ - public function at(string|int $k): mixed; + public function at(int|string $k): mixed; /** * Determines if the specified key is in the current vector. @@ -42,7 +42,7 @@ public function contains(int|string $k): bool; * * @psalm-mutation-free */ - public function get(string|int $k): mixed; + public function get(int|string $k): mixed; /** * Get an array copy of the current vector. diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php index a2310df0..5b365c44 100644 --- a/src/Psl/Internal/Loader.php +++ b/src/Psl/Internal/Loader.php @@ -341,6 +341,8 @@ final class Loader 'Psl\\sequence' => 'Psl/sequence.php', 'Psl\\Type\\map' => 'Psl/Type/map.php', 'Psl\\Type\\mutable_map' => 'Psl/Type/mutable_map.php', + 'Psl\\Type\\set' => 'Psl/Type/set.php', + 'Psl\\Type\\mutable_set' => 'Psl/Type/mutable_set.php', 'Psl\\Type\\vector' => 'Psl/Type/vector.php', 'Psl\\Type\\mutable_vector' => 'Psl/Type/mutable_vector.php', 'Psl\\Type\\array_key' => 'Psl/Type/array_key.php', @@ -573,6 +575,8 @@ final class Loader 'Psl\\Collection\\MutableVectorInterface' => 'Psl/Collection/MutableVectorInterface.php', 'Psl\\Collection\\MapInterface' => 'Psl/Collection/MapInterface.php', 'Psl\\Collection\\MutableMapInterface' => 'Psl/Collection/MutableMapInterface.php', + 'Psl\\Collection\\SetInterface' => 'Psl/Collection/SetInterface.php', + 'Psl\\Collection\\MutableSetInterface' => 'Psl/Collection/MutableSetInterface.php', 'Psl\\Observer\\SubjectInterface' => 'Psl/Observer/SubjectInterface.php', 'Psl\\Observer\\ObserverInterface' => 'Psl/Observer/ObserverInterface.php', 'Psl\\Result\\ResultInterface' => 'Psl/Result/ResultInterface.php', @@ -671,6 +675,8 @@ final class Loader 'Psl\\Collection\\MutableVector' => 'Psl/Collection/MutableVector.php', 'Psl\\Collection\\Map' => 'Psl/Collection/Map.php', 'Psl\\Collection\\MutableMap' => 'Psl/Collection/MutableMap.php', + 'Psl\\Collection\\Set' => 'Psl/Collection/Set.php', + 'Psl\\Collection\\MutableSet' => 'Psl/Collection/MutableSet.php', 'Psl\\Encoding\\Base64\\Internal\\Base64' => 'Psl/Encoding/Base64/Internal/Base64.php', 'Psl\\Encoding\\Base64\\Internal\\Base64UrlSafe' => 'Psl/Encoding/Base64/Internal/Base64UrlSafe.php', 'Psl\\Encoding\\Base64\\Internal\\Base64DotSlash' => 'Psl/Encoding/Base64/Internal/Base64DotSlash.php', @@ -689,6 +695,8 @@ final class Loader 'Psl\\Type\\Internal\\ArrayKeyType' => 'Psl/Type/Internal/ArrayKeyType.php', 'Psl\\Type\\Internal\\MapType' => 'Psl/Type/Internal/MapType.php', 'Psl\\Type\\Internal\\MutableMapType' => 'Psl/Type/Internal/MutableMapType.php', + 'Psl\\Type\\Internal\\SetType' => 'Psl/Type/Internal/SetType.php', + 'Psl\\Type\\Internal\\MutableSetType' => 'Psl/Type/Internal/MutableSetType.php', 'Psl\\Type\\Internal\\VectorType' => 'Psl/Type/Internal/VectorType.php', 'Psl\\Type\\Internal\\MutableVectorType' => 'Psl/Type/Internal/MutableVectorType.php', 'Psl\\Type\\Internal\\BoolType' => 'Psl/Type/Internal/BoolType.php', @@ -832,6 +840,8 @@ final class Loader 'Psl\\Iter\\Exception\\OutOfBoundsException' => 'Psl/Iter/Exception/OutOfBoundsException.php', 'Psl\\Str\\Exception\\OutOfBoundsException' => 'Psl/Str/Exception/OutOfBoundsException.php', 'Psl\\Collection\\Exception\\OutOfBoundsException' => 'Psl/Collection/Exception/OutOfBoundsException.php', + 'Psl\\Collection\\Exception\\InvalidOffsetException' => 'Psl/Collection/Exception/InvalidOffsetException.php', + 'Psl\\Collection\\Exception\\RuntimeException' => 'Psl/Collection/Exception/RuntimeException.php', 'Psl\\DataStructure\\Exception\\UnderflowException' => 'Psl/DataStructure/Exception/UnderflowException.php', 'Psl\\Vec\\Exception\\LogicException' => 'Psl/Vec/Exception/LogicException.php', 'Psl\\File\\Exception\\AlreadyCreatedException' => 'Psl/File/Exception/AlreadyCreatedException.php', diff --git a/src/Psl/Type/Internal/MutableSetType.php b/src/Psl/Type/Internal/MutableSetType.php new file mode 100644 index 00000000..dac089f4 --- /dev/null +++ b/src/Psl/Type/Internal/MutableSetType.php @@ -0,0 +1,126 @@ +> + * + * @internal + */ +final readonly class MutableSetType extends Type\Type +{ + /** + * @psalm-mutation-free + * + * @param Type\TypeInterface $type + */ + public function __construct( + private readonly Type\TypeInterface $type, + ) { + } + + /** + * @throws CoercionException + * + * @return Collection\MutableSetInterface + */ + public function coerce(mixed $value): Collection\MutableSetInterface + { + if (is_iterable($value)) { + /** @var Type\Type $type */ + $type = $this->type; + /** @var array $set */ + $set = []; + $k = null; + $v = null; + $iterating = true; + try { + /** + * @var array-key $k + * @var T $v + */ + foreach ($value as $k => $v) { + $iterating = false; + $v = $type->coerce($v); + $set[$v] = $v; + $iterating = true; + } + } catch (Throwable $e) { + if ($iterating) { + throw CoercionException::withValue(null, $this->toString(), PathExpression::iteratorError($k), $e); + } + + throw CoercionException::withValue($v, $this->toString(), PathExpression::path($k), $e); + } + + /** @var Collection\MutableSet */ + return new Collection\MutableSet($set); + } + + throw CoercionException::withValue($value, $this->toString()); + } + + /** + * @throws AssertException + * + * @return Collection\MutableSetInterface + * + * @psalm-assert Collection\MutableSetInterface $value + */ + public function assert(mixed $value): Collection\MutableSetInterface + { + if (is_object($value) && $value instanceof Collection\MutableSetInterface) { + /** @var Type\Type $type */ + $type = $this->type; + /** @var array $set */ + $set = []; + $k = $v = null; + $iterating = true; + try { + /** + * @var T $v + */ + foreach ($value as $k => $v) { + $iterating = false; + $v = $type->assert($v); + $set[$v] = $v; + $iterating = true; + } + } catch (Throwable $e) { + if ($iterating) { + throw AssertException::withValue(null, $this->toString(), PathExpression::iteratorError($k), $e); + } + + throw AssertException::withValue($v, $this->toString(), PathExpression::path($k), $e); + } + + /** @var Collection\MutableSet */ + return new Collection\MutableSet($set); + } + + throw AssertException::withValue($value, $this->toString()); + } + + public function toString(): string + { + return Str\format( + '%s<%s>', + Collection\MutableSetInterface::class, + $this->type->toString(), + ); + } +} diff --git a/src/Psl/Type/Internal/SetType.php b/src/Psl/Type/Internal/SetType.php new file mode 100644 index 00000000..41cd85b9 --- /dev/null +++ b/src/Psl/Type/Internal/SetType.php @@ -0,0 +1,126 @@ +> + * + * @internal + */ +final readonly class SetType extends Type\Type +{ + /** + * @psalm-mutation-free + * + * @param Type\TypeInterface $type + */ + public function __construct( + private readonly Type\TypeInterface $type, + ) { + } + + /** + * @throws CoercionException + * + * @return Collection\SetInterface + */ + public function coerce(mixed $value): Collection\SetInterface + { + if (is_iterable($value)) { + /** @var Type\Type $type */ + $type = $this->type; + /** @var array $set */ + $set = []; + $k = null; + $v = null; + $iterating = true; + try { + /** + * @var array-key $k + * @var T $v + */ + foreach ($value as $k => $v) { + $iterating = false; + $v = $type->coerce($v); + $set[$v] = $v; + $iterating = true; + } + } catch (Throwable $e) { + if ($iterating) { + throw CoercionException::withValue(null, $this->toString(), PathExpression::iteratorError($k), $e); + } + + throw CoercionException::withValue($v, $this->toString(), PathExpression::path($k), $e); + } + + /** @var Collection\Set */ + return new Collection\Set($set); + } + + throw CoercionException::withValue($value, $this->toString()); + } + + /** + * @throws AssertException + * + * @return Collection\SetInterface + * + * @psalm-assert Collection\SetInterface $value + */ + public function assert(mixed $value): Collection\SetInterface + { + if (is_object($value) && $value instanceof Collection\SetInterface) { + /** @var Type\Type $type */ + $type = $this->type; + /** @var array $set */ + $set = []; + $v = $k = null; + $iterating = true; + try { + /** + * @var T $v + */ + foreach ($value as $k => $v) { + $iterating = false; + $v = $type->assert($v); + $set[$v] = $v; + $iterating = true; + } + } catch (Throwable $e) { + if ($iterating) { + throw AssertException::withValue(null, $this->toString(), PathExpression::iteratorError($k), $e); + } + + throw AssertException::withValue($v, $this->toString(), PathExpression::path($k), $e); + } + + /** @var Collection\Set */ + return new Collection\Set($set); + } + + throw AssertException::withValue($value, $this->toString()); + } + + public function toString(): string + { + return Str\format( + '%s<%s>', + Collection\SetInterface::class, + $this->type->toString(), + ); + } +} diff --git a/src/Psl/Type/mutable_set.php b/src/Psl/Type/mutable_set.php new file mode 100644 index 00000000..a4b6bfe6 --- /dev/null +++ b/src/Psl/Type/mutable_set.php @@ -0,0 +1,21 @@ + $key_type + * + * @return TypeInterface> + */ +function mutable_set(TypeInterface $key_type): TypeInterface +{ + return new Internal\MutableSetType($key_type); +} diff --git a/src/Psl/Type/set.php b/src/Psl/Type/set.php new file mode 100644 index 00000000..93bf1250 --- /dev/null +++ b/src/Psl/Type/set.php @@ -0,0 +1,21 @@ + $key_type + * + * @return TypeInterface> + */ +function set(TypeInterface $key_type): TypeInterface +{ + return new Internal\SetType($key_type); +} diff --git a/tests/unit/Collection/AbstractSetTest.php b/tests/unit/Collection/AbstractSetTest.php new file mode 100644 index 00000000..759cc770 --- /dev/null +++ b/tests/unit/Collection/AbstractSetTest.php @@ -0,0 +1,534 @@ + + */ + protected string $setClass = SetInterface::class; + + public function testIsEmpty(): void + { + static::assertTrue($this->default()->isEmpty()); + static::assertTrue($this->createFromList([])->isEmpty()); + static::assertFalse($this->createFromList(['foo', 'bar'])->isEmpty()); + static::assertFalse($this->createFromList([1])->isEmpty()); + } + + public function testCount(): void + { + static::assertCount(0, $this->default()); + static::assertCount(0, $this->createFromList([])); + static::assertCount(2, $this->createFromList(['foo', 'bar'])); + static::assertSame(5, $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + 'hax' // ?? + ])->count()); + } + + public function testValues(): void + { + $vector = $this->createFromList([1, 2, 3]); + + $values = $vector->values(); + + static::assertCount(3, $values); + + static::assertSame(1, $values->at(0)); + static::assertSame(2, $values->at(1)); + static::assertSame(3, $values->at(2)); + + $vector = $this->createFromList([]); + $values = $vector->values(); + + static::assertCount(0, $values); + } + + public function testJsonSerialize(): void + { + $vector = $this->createFromList(['foo', 'bar', 'baz']); + + $array = $vector->jsonSerialize(); + + static::assertSame(['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'], $array); + } + + public function testKeys(): void + { + $vector = $this->createFromList([ + 'foo', + 'bar', + 'baz', + ]); + $keys = $vector->keys(); + + static::assertCount(3, $keys); + static::assertSame('foo', $keys->at(0)); + static::assertSame('bar', $keys->at(1)); + static::assertSame('baz', $keys->at(2)); + + $vector = $this->createFromList([]); + $keys = $vector->keys(); + + static::assertCount(0, $keys); + } + + public function testFilter(): void + { + $vector = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $filtered = $vector->filter(static fn (string $item) => Str\contains($item, 'b')); + + static::assertInstanceOf($this->setClass, $filtered); + static::assertNotSame($vector, $filtered); + static::assertContains('bar', $filtered); + static::assertContains('baz', $filtered); + static::assertNotContains('foo', $filtered); + static::assertNotContains('qux', $filtered); + static::assertCount(2, $filtered); + + $vector = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $filtered = $vector->filter(static fn (string $item) => Str\contains($item, 'hello')); + + static::assertInstanceOf($this->setClass, $filtered); + static::assertNotContains('bar', $filtered); + static::assertNotContains('baz', $filtered); + static::assertNotContains('foo', $filtered); + static::assertNotContains('qux', $filtered); + static::assertCount(0, $filtered); + } + + public function testFilterWithKey(): void + { + $vector = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $filtered = $vector->filterWithKey(static fn (string $item) => Str\contains($item, 'b')); + + static::assertInstanceOf($this->setClass, $filtered); + static::assertNotSame($vector, $filtered); + static::assertContains('bar', $filtered); + static::assertContains('baz', $filtered); + static::assertNotContains('foo', $filtered); + static::assertNotContains('qux', $filtered); + static::assertCount(2, $filtered); + + $vector = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $filtered = $vector->filterWithKey(static fn (string $item) => Str\contains($item, 'hello')); + + static::assertInstanceOf($this->setClass, $filtered); + static::assertNotContains('bar', $filtered); + static::assertNotContains('baz', $filtered); + static::assertNotContains('foo', $filtered); + static::assertNotContains('qux', $filtered); + static::assertCount(0, $filtered); + } + + public function testMap(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $mapped = $set->map(static fn (string $item) => Str\uppercase($item)); + + static::assertInstanceOf($this->setClass, $mapped); + static::assertSame([ + 'FOO' => 'FOO', + 'BAR' => 'BAR', + 'BAZ' => 'BAZ', + 'QUX' => 'QUX', + ], $mapped->toArray()); + static::assertNotSame($set, $mapped); + static::assertCount(4, $mapped); + + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $mapped = $set->map(static fn (string $item) => $item); + + static::assertInstanceOf($this->setClass, $mapped); + static::assertNotSame($set, $mapped); + static::assertSame($set->toArray(), $mapped->toArray()); + static::assertCount(4, $mapped); + } + public function testMapWithKey(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $mapped = $set->mapWithKey(static fn (string $item) => Str\uppercase($item)); + + static::assertInstanceOf($this->setClass, $mapped); + static::assertSame([ + 'FOO' => 'FOO', + 'BAR' => 'BAR', + 'BAZ' => 'BAZ', + 'QUX' => 'QUX', + ], $mapped->toArray()); + static::assertNotSame($set, $mapped); + static::assertCount(4, $mapped); + + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $mapped = $set->mapWithKey(static fn (string $item) => $item); + + static::assertInstanceOf($this->setClass, $mapped); + static::assertNotSame($set, $mapped); + static::assertSame($set->toArray(), $mapped->toArray()); + static::assertCount(4, $mapped); + } + public function testZip(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $other = $this->createFromList([ + 'hello', + 'world', + 'foo', + 'bar', + ]); + + $this->expectException(Collection\Exception\RuntimeException::class); + + $set->zip($other->toArray()); + } + + public function testFirst(): void + { + $vector = $this->createFromList([]); + static::assertNull($vector->first()); + + $vector = $this->createFromList(['foo']); + static::assertSame('foo', $vector->first()); + + $vector = $this->createFromList(['bar', 'qux']); + static::assertSame('bar', $vector->first()); + } + + public function testFirstKey(): void + { + $vector = $this->createFromList([]); + static::assertNull($vector->firstKey()); + + $vector = $this->createFromList(['foo']); + static::assertSame('foo', $vector->firstKey()); + + $vector = $this->createFromList(['bar', 'qux']); + static::assertSame('bar', $vector->firstKey()); + } + + public function testLast(): void + { + $vector = $this->createFromList([]); + static::assertNull($vector->last()); + + $vector = $this->createFromList(['foo']); + static::assertSame('foo', $vector->last()); + + $vector = $this->createFromList(['bar', 'qux']); + static::assertSame('qux', $vector->last()); + } + + public function testLastKey(): void + { + $vector = $this->createFromList([]); + static::assertNull($vector->lastKey()); + + $vector = $this->createFromList(['foo']); + static::assertSame('foo', $vector->lastKey()); + + $vector = $this->createFromList(['bar', 'qux']); + static::assertSame('qux', $vector->lastKey()); + } + + public function testLinearSearch(): void + { + $vector = $this->createFromList([]); + static::assertNull($vector->linearSearch('foo')); + + $vector = $this->createFromList([ + 'foo', + 'bar', + ]); + static::assertSame('foo', $vector->linearSearch('foo')); + static::assertSame('bar', $vector->linearSearch('bar')); + static::assertNull($vector->linearSearch('baz')); + static::assertNull($vector->linearSearch('qux')); + } + + public function testTake(): void + { + $set = $this->default(); + $rest = $set->take(2); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(0, $rest); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->take(4); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(2, $rest); + static::assertSame($set->toArray(), $rest->toArray()); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->take(1); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(1, $rest); + static::assertSame('bar', $rest->at('bar')); + } + + public function testTakeWhile(): void + { + $set = $this->default(); + $rest = $set->takeWhile(static fn ($v) => false); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(0, $rest); + + $set = $this->default(); + $rest = $set->takeWhile(static fn ($v) => true); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(0, $rest); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->takeWhile(static fn ($v) => true); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(2, $rest); + static::assertSame($set->toArray(), $rest->toArray()); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->takeWhile(static fn ($v) => 'bar' === $v); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(1, $rest); + static::assertSame('bar', $rest->at('bar')); + } + + public function testDrop(): void + { + $set = $this->default(); + $rest = $set->drop(2); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(0, $rest); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->drop(4); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(0, $rest); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->drop(1); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(1, $rest); + static::assertSame('qux', $rest->at('qux')); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->drop(0); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(2, $rest); + static::assertSame($set->toArray(), $rest->toArray()); + } + + public function testDropWhile(): void + { + $set = $this->default(); + $rest = $set->dropWhile(static fn ($v) => true); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(0, $rest); + + $set = $this->default(); + $rest = $set->dropWhile(static fn ($v) => false); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(0, $rest); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->dropWhile(static fn ($v) => true); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(0, $rest); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->dropWhile(static fn ($v) => false); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(2, $rest); + static::assertSame($set->toArray(), $rest->toArray()); + + $set = $this->createFromList(['bar', 'qux']); + $rest = $set->dropWhile(static fn ($v) => 'bar' === $v); + static::assertInstanceOf($this->setClass, $rest); + static::assertNotSame($set, $rest); + static::assertCount(1, $rest); + static::assertSame('qux', $rest->at('qux')); + } + + public function testSlice(): void + { + $vector = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + 'hax', + 'dax', + 'lax', + 'fax', + ]); + + $slice1 = $vector->slice(0, 1); + static::assertInstanceOf($this->setClass, $slice1); + static::assertNotSame($slice1, $vector); + static::assertCount(1, $slice1); + static::assertSame('foo', $slice1->at('foo')); + + $slice2 = $vector->slice(2, 4); + static::assertInstanceOf($this->setClass, $slice1); + static::assertNotSame($slice2, $vector); + static::assertCount(4, $slice2); + static::assertSame([ + 'baz' => 'baz', + 'qux' => 'qux', + 'hax' => 'hax', + 'dax' => 'dax', + ], $slice2->toArray()); + } + + public function testAt(): void + { + $set = $this->createFromList([ + 'hello', + 'world', + ]); + + static::assertSame('hello', $set->at('hello')); + static::assertSame('world', $set->at('world')); + + $this->expectException(Collection\Exception\OutOfBoundsException::class); + $this->expectExceptionMessage('Key (foo) was out-of-bounds.'); + + $set->at('foo'); + } + + public function testContains(): void + { + $vector = $this->createFromList([ + 'hello', + 'world', + ]); + + static::assertTrue($vector->contains('hello')); + static::assertTrue($vector->contains('world')); + static::assertFalse($vector->contains('foo')); + } + + public function testGet(): void + { + $vector = $this->createFromList([ + 'hello', + 'world', + ]); + + static::assertSame('hello', $vector->get('hello')); + static::assertSame('world', $vector->get('world')); + static::assertNull($vector->get('foo')); + } + + public function testChunk(): void + { + $set = $this->createFromList(['foo', 'bar', 'baz']); + + $chunks = $set->chunk(2); + + static::assertCount(2, $chunks); + static::assertSame(['foo' => 'foo', 'bar' => 'bar'], $chunks->at(0)->toArray()); + static::assertSame(['baz' => 'baz'], $chunks->at(1)->toArray()); + + $chunks = $set->chunk(1); + + static::assertCount(3, $chunks); + static::assertSame(['foo' => 'foo'], $chunks->at(0)->toArray()); + static::assertSame(['bar' => 'bar'], $chunks->at(1)->toArray()); + static::assertSame(['baz' => 'baz'], $chunks->at(2)->toArray()); + } + + protected function default(): SetInterface + { + return ($this->setClass)::default(); + } + + /** + * @template T of array-key + * + * @param list $items + * + * @return SetInterface + */ + abstract protected function createFromList(array $items): SetInterface; +} diff --git a/tests/unit/Collection/AbstractVectorTest.php b/tests/unit/Collection/AbstractVectorTest.php index 1082ce7b..142696db 100644 --- a/tests/unit/Collection/AbstractVectorTest.php +++ b/tests/unit/Collection/AbstractVectorTest.php @@ -568,11 +568,11 @@ protected function default(): VectorInterface } /** - * @template T + * @template T * - * @param iterable $items + * @param list $items * * @return VectorInterface */ - abstract protected function create(iterable $items): VectorInterface; + abstract protected function create(array $items): VectorInterface; } diff --git a/tests/unit/Collection/MutableMapTest.php b/tests/unit/Collection/MutableMapTest.php index 6896e8b7..619a71ca 100644 --- a/tests/unit/Collection/MutableMapTest.php +++ b/tests/unit/Collection/MutableMapTest.php @@ -5,6 +5,7 @@ namespace Psl\Tests\Unit\Collection; use Psl\Collection; +use Psl\Collection\Exception; use Psl\Collection\MutableMap; use Psl\Collection\MutableVector; @@ -142,6 +143,98 @@ public function testRemove(): void static::assertNull($map->get('bar')); } + public function testArrayAccess(): void + { + $map = $this->create([ + 'foo' => '1', + 'bar' => '2', + 'baz' => '3', + ]); + + static::assertTrue(isset($map['foo'])); + static::assertSame('1', $map['foo']); + + unset($map['foo']); + static::assertFalse(isset($map['foo'])); + + $map['foo'] = '2'; + static::assertTrue(isset($map['foo'])); + static::assertSame('2', $map['foo']); + + $map['qux'] = '4'; + static::assertTrue(isset($map['qux'])); + static::assertCount(4, $map); + + $map[124] = 'v'; + static::assertTrue(isset($map[124])); + static::assertSame('v', $map[124]); + static::assertCount(5, $map); + + unset($map[124]); + + $this->expectException(Exception\OutOfBoundsException::class); + $this->expectExceptionMessage('Key (124) was out-of-bounds.'); + + $map[124]; + } + + public function testOffsetSetThrowsForInvalidOffsetType(): void + { + $map = $this->create([ + 'foo' => '1', + 'bar' => '2', + 'baz' => '3', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid map write offset type, expected a string or an integer.'); + + $map[false] = 'qux'; + } + + public function testOffsetIssetThrowsForInvalidOffsetType(): void + { + $map = $this->create([ + 'foo' => '1', + 'bar' => '2', + 'baz' => '3', + ]); + + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid map read offset type, expected a string or an integer.'); + + isset($map[false]); + } + + public function testOffsetUnsetThrowsForInvalidOffsetType(): void + { + $map = $this->create([ + 'foo' => '1', + 'bar' => '2', + 'baz' => '3', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid map read offset type, expected a string or an integer.'); + + unset($map[false]); + } + + public function testOffsetGetThrowsForInvalidOffsetType(): void + { + $map = $this->create([ + 'foo' => '1', + 'bar' => '2', + 'baz' => '3', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid map read offset type, expected a string or an integer.'); + + $map[false]; + } + /** * @template Tk of array-key * @template Tv diff --git a/tests/unit/Collection/MutableSetTest.php b/tests/unit/Collection/MutableSetTest.php new file mode 100644 index 00000000..415d2643 --- /dev/null +++ b/tests/unit/Collection/MutableSetTest.php @@ -0,0 +1,205 @@ + + */ + protected string $setClass = MutableSet::class; + + + public function testClear(): void + { + $set = $this->createFromList(['foo', 'bar']); + $cleared = $set->clear(); + + static::assertSame($cleared, $set); + static::assertCount(0, $set); + } + + public function testAdd(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $modified = $set + ->add('foo') + ->add('bar') + ->add('baz') + ->add('qux') + ; + + static::assertSame($modified, $set); + + static::assertSame('foo', $set->at('foo')); + static::assertSame('bar', $set->at('bar')); + static::assertSame('baz', $set->at('baz')); + static::assertSame('qux', $set->at('qux')); + static::assertCount(4, $set); + + $set->add('quux'); + + static::assertSame('quux', $set->at('quux')); + static::assertCount(5, $set); + } + + public function testAddAll(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + 'qux', + ]); + + $modified = $set->addAll(['foo', 'bar', 'baz', 'qux']); + + static::assertSame($modified, $set); + + static::assertSame('foo', $set->at('foo')); + static::assertSame('bar', $set->at('bar')); + static::assertSame('baz', $set->at('baz')); + static::assertSame('qux', $set->at('qux')); + static::assertCount(4, $set); + + $set->addAll(['quux', 'corge']); + + static::assertSame('quux', $set->at('quux')); + static::assertSame('corge', $set->at('corge')); + static::assertCount(6, $set); + } + + public function testRemove(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + ]); + + $modified = $set + ->remove('foo') + ->remove('bar'); + + static::assertSame($modified, $set); + static::assertCount(1, $set); + static::assertSame('baz', $set->values()->at(0)); + } + + public function testArrayAccess(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + ]); + + static::assertTrue(isset($set['foo'])); + static::assertSame('foo', $set['foo']); + + unset($set['foo']); + static::assertFalse(isset($set['foo'])); + + $set['foo'] = 'foo'; + static::assertTrue(isset($set['foo'])); + + $set[] = 'qux'; + static::assertTrue(isset($set['qux'])); + static::assertCount(4, $set); + } + + public function testOffsetSetThrowsForInvalidOffsetType(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid set write offset type, expected null or the same as the value.'); + + $set[false] = 'qux'; + } + + public function testOffsetSetThrowsForInvalidOffsetValue(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid set write offset type, expected null or the same as the value.'); + + $set[0] = 'qux'; + } + + public function testOffsetIssetThrowsForInvalidOffsetType(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid set read offset type, expected a string or an integer.'); + + isset($set[false]); + } + + public function testOffsetUnsetThrowsForInvalidOffsetType(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid set read offset type, expected a string or an integer.'); + + unset($set[false]); + } + + public function testOffsetGetThrowsForInvalidOffsetType(): void + { + $set = $this->createFromList([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid set read offset type, expected a string or an integer.'); + + $set[false]; + } + + /** + * @template T of array-key + * + * @param array $items + * + * @return MutableSet + */ + protected function createFromList(array $items): MutableSet + { + return MutableSet::fromArray($items); + } +} diff --git a/tests/unit/Collection/MutableVectorTest.php b/tests/unit/Collection/MutableVectorTest.php index 68bcea2f..2fd0e75a 100644 --- a/tests/unit/Collection/MutableVectorTest.php +++ b/tests/unit/Collection/MutableVectorTest.php @@ -5,6 +5,7 @@ namespace Psl\Tests\Unit\Collection; use Psl\Collection; +use Psl\Collection\Exception; use Psl\Collection\MutableVector; final class MutableVectorTest extends AbstractVectorTest @@ -137,14 +138,107 @@ public function testRemove(): void static::assertSame('baz', $vector->get(0)); } + + public function testArrayAccess(): void + { + $vector = $this->create([ + 'foo', + 'bar', + 'baz', + ]); + + static::assertTrue(isset($vector[0])); + static::assertSame('foo', $vector[0]); + + unset($vector[0]); + static::assertFalse(isset($vector[2])); + + $vector[] = 'foo'; + static::assertTrue(isset($vector[2])); + static::assertSame('foo', $vector[2]); + + $vector[] = 'qux'; + static::assertTrue(isset($vector[3])); + static::assertCount(4, $vector); + + $vector[2] = 'v'; + static::assertTrue(isset($vector[2])); + static::assertSame('v', $vector[2]); + static::assertCount(4, $vector); + + unset($vector[3]); + + $this->expectException(Exception\OutOfBoundsException::class); + $this->expectExceptionMessage('Key (3) was out-of-bounds.'); + + $vector[3]; + } + + public function testOffsetSetThrowsForInvalidOffsetType(): void + { + $vector = $this->create([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid vector write offset type, expected a positive integer or null.'); + + $vector[false] = 'qux'; + } + + public function testOffsetIssetThrowsForInvalidOffsetType(): void + { + $vector = $this->create([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid vector read offset type, expected a positive integer.'); + + isset($vector[false]); + } + + public function testOffsetUnsetThrowsForInvalidOffsetType(): void + { + $vector = $this->create([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid vector read offset type, expected a positive integer.'); + + unset($vector[false]); + } + + public function testOffsetGetThrowsForInvalidOffsetType(): void + { + $vector = $this->create([ + 'foo', + 'bar', + 'baz', + ]); + + $this->expectException(Exception\InvalidOffsetException::class); + $this->expectExceptionMessage('Invalid vector read offset type, expected a positive integer.'); + + $vector[false]; + } + + /** * @template T * - * @param iterable $items + * @param list $items * * @return MutableVector */ - protected function create(iterable $items): MutableVector + protected function create(array $items): MutableVector { return new MutableVector($items); } diff --git a/tests/unit/Collection/SetTest.php b/tests/unit/Collection/SetTest.php new file mode 100644 index 00000000..fedc4f6e --- /dev/null +++ b/tests/unit/Collection/SetTest.php @@ -0,0 +1,29 @@ + + */ + protected string $setClass = Set::class; + + /** + * @template T of array-key + * + * @param array $items + * + * @return Set + */ + protected function createFromList(array $items): Set + { + return Set::fromArray($items); + } +} diff --git a/tests/unit/Collection/VectorTest.php b/tests/unit/Collection/VectorTest.php index 91397098..24afb718 100644 --- a/tests/unit/Collection/VectorTest.php +++ b/tests/unit/Collection/VectorTest.php @@ -16,13 +16,13 @@ final class VectorTest extends AbstractVectorTest protected string $vectorClass = Vector::class; /** - * @template T + * @template T * - * @param iterable $items + * @param array $items * * @return Vector */ - protected function create(iterable $items): Vector + protected function create(array $items): Vector { return new Vector($items); } diff --git a/tests/unit/Type/MutableSetTypeTest.php b/tests/unit/Type/MutableSetTypeTest.php new file mode 100644 index 00000000..1cca9dc1 --- /dev/null +++ b/tests/unit/Type/MutableSetTypeTest.php @@ -0,0 +1,195 @@ +> + */ +final class MutableSetTypeTest extends TypeTest +{ + public function getType(): Type\TypeInterface + { + return Type\mutable_set(Type\int()); + } + + public function getValidCoercions(): iterable + { + yield [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + new Collection\MutableSet([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + ]; + + yield [ + Vec\range(1, 10), + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + Dict\map(Vec\range(1, 10), static fn(int $key): string => (string)$key), + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + new Collection\Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + new Collection\MutableVector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + new Collection\Vector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + } + + public function getInvalidCoercions(): iterable + { + yield [1.0]; + yield [1.23]; + yield [Type\bool()]; + yield [null]; + yield [false]; + yield [true]; + yield [STDIN]; + } + + public function getToStringExamples(): iterable + { + yield [$this->getType(), 'Psl\Collection\MutableSetInterface']; + yield [Type\mutable_set(Type\string()), 'Psl\Collection\MutableSetInterface']; + } + + /** + * @param MutableSetInterface|mixed $a + * @param MutableSetInterface|mixed $b + */ + protected function equals($a, $b): bool + { + if (Type\instance_of(MutableSetInterface::class)->matches($a)) { + $a = $a->toArray(); + } + + if (Type\instance_of(MutableSetInterface::class)->matches($b)) { + $b = $b->toArray(); + } + + return parent::equals($a, $b); + } + + public static function provideAssertExceptionExpectations(): iterable + { + yield 'invalid assertion value' => [ + Type\mutable_set(Type\int()), + new Collection\MutableSet(['nope' => 'nope']), + 'Expected "' . MutableSetInterface::class . '", got "string" at path "nope".' + ]; + yield 'nested' => [ + Type\mutable_set(Type\string()), + new Collection\MutableSet([123 => 123]), + 'Expected "' . MutableSetInterface::class . '", got "int" at path "123".' + ]; + } + + public static function provideCoerceExceptionExpectations(): iterable + { + yield 'invalid coercion value' => [ + Type\mutable_set(Type\int()), + ['nope' => 'nope'], + 'Could not coerce "string" to type "' . MutableSetInterface::class . '" at path "nope".' + ]; + yield 'invalid iterator first item' => [ + Type\mutable_set(Type\int()), + (static function () { + yield Type\int()->coerce('nope'); + })(), + 'Could not coerce "string" to type "' . MutableSetInterface::class . '" at path "first()".' + ]; + yield 'invalid iterator second item' => [ + Type\mutable_set(Type\int()), + (static function () { + yield 0; + yield Type\int()->coerce('nope'); + })(), + 'Could not coerce "string" to type "' . MutableSetInterface::class . '" at path "0.next()".' + ]; + yield 'iterator throwing exception' => [ + Type\mutable_set(Type\int()), + (static function () { + yield 0; + throw new RuntimeException('whoops'); + })(), + 'Could not coerce "null" to type "' . MutableSetInterface::class . '" at path "0.next()": whoops.' + ]; + yield 'iterator yielding null key' => [ + Type\mutable_set(Type\int()), + (static function () { + yield null => 'nope'; + })(), + 'Could not coerce "string" to type "' . MutableSetInterface::class . '" at path "null".' + ]; + yield 'iterator yielding string key, null value' => [ + Type\mutable_set(Type\int()), + (static function () { + yield 'nope' => null; + })(), + 'Could not coerce "null" to type "' . MutableSetInterface::class . '" at path "nope".' + ]; + yield 'iterator yielding object key' => [ + Type\mutable_set(Type\int()), + (static function () { + yield 'nope' => (new class () { + }); + })(), + 'Could not coerce "class@anonymous" to type "' . MutableSetInterface::class . '" at path "nope".' + ]; + } + + /** + * @dataProvider provideAssertExceptionExpectations + */ + public function testInvalidAssertionTypeExceptions(Type\TypeInterface $type, mixed $data, string $expectedMessage): void + { + try { + $type->assert($data); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame($expectedMessage, $e->getMessage()); + } + } + + /** + * @dataProvider provideCoerceExceptionExpectations + */ + public function testInvalidCoercionTypeExceptions(Type\TypeInterface $type, mixed $data, string $expectedMessage): void + { + try { + $type->coerce($data); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\CoercionException::class)); + } catch (Type\Exception\CoercionException $e) { + static::assertSame($expectedMessage, $e->getMessage()); + } + } +} diff --git a/tests/unit/Type/SetTypeTest.php b/tests/unit/Type/SetTypeTest.php new file mode 100644 index 00000000..520857df --- /dev/null +++ b/tests/unit/Type/SetTypeTest.php @@ -0,0 +1,191 @@ +> + */ +final class SetTypeTest extends TypeTest +{ + public function getType(): Type\TypeInterface + { + return Type\set(Type\int()); + } + + public function getValidCoercions(): iterable + { + + yield [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + new Collection\Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], + new Collection\Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + ]; + + yield [ + Vec\range(1, 10), + new Collection\Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + Dict\map(Vec\range(1, 10), static fn(int $key): string => (string)$key), + new Collection\Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + new Collection\Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + new Collection\Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + new Collection\MutableSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + new Collection\Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + + yield [ + new Collection\MutableVector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + new Collection\Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + ]; + } + + public function getInvalidCoercions(): iterable + { + yield [1.0]; + yield [1.23]; + yield [Type\bool()]; + yield [null]; + yield [false]; + yield [true]; + yield [STDIN]; + } + + public function getToStringExamples(): iterable + { + yield [$this->getType(), 'Psl\Collection\SetInterface']; + yield [Type\set(Type\string()), 'Psl\Collection\SetInterface']; + } + + /** + * @param SetInterface|mixed $a + * @param SetInterface|mixed $b + */ + protected function equals($a, $b): bool + { + if (Type\instance_of(SetInterface::class)->matches($a)) { + $a = $a->toArray(); + } + + if (Type\instance_of(SetInterface::class)->matches($b)) { + $b = $b->toArray(); + } + + return parent::equals($a, $b); + } + + public static function provideAssertExceptionExpectations(): iterable + { + yield 'invalid assertion value' => [ + Type\set(Type\int()), + new Collection\MutableSet(['foo' => 'nope']), + 'Expected "' . SetInterface::class . '", got "string" at path "nope".' + ]; + yield 'nested' => [ + Type\set(Type\string()), + new Collection\MutableSet([1 => 123]), + 'Expected "' . SetInterface::class . '", got "int" at path "123".' + ]; + } + + public static function provideCoerceExceptionExpectations(): iterable + { + yield 'invalid coercion value' => [ + Type\set(Type\int()), + ['nope' => 'nope'], + 'Could not coerce "string" to type "' . SetInterface::class . '" at path "nope".' + ]; + yield 'invalid iterator first item' => [ + Type\set(Type\int()), + (static function () { + yield Type\int()->coerce('nope'); + })(), + 'Could not coerce "string" to type "' . SetInterface::class . '" at path "first()".' + ]; + yield 'invalid iterator second item' => [ + Type\set(Type\int()), + (static function () { + yield 0; + yield Type\int()->coerce('nope'); + })(), + 'Could not coerce "string" to type "' . SetInterface::class . '" at path "0.next()".' + ]; + yield 'iterator throwing exception' => [ + Type\set(Type\int()), + (static function () { + yield 0; + throw new RuntimeException('whoops'); + })(), + 'Could not coerce "null" to type "' . SetInterface::class . '" at path "0.next()": whoops.' + ]; + yield 'iterator yielding null key' => [ + Type\set(Type\int()), + (static function () { + yield null => 'nope'; + })(), + 'Could not coerce "string" to type "' . SetInterface::class . '" at path "null".' + ]; + yield 'iterator yielding string key, null value' => [ + Type\set(Type\int()), + (static function () { + yield 'nope' => 'bar'; + })(), + 'Could not coerce "string" to type "' . SetInterface::class . '" at path "nope".' + ]; + yield 'iterator yielding object key' => [ + Type\set(Type\int()), + (static function () { + yield 'nope' => (new class () { + }); + })(), + 'Could not coerce "class@anonymous" to type "' . SetInterface::class . '" at path "nope".' + ]; + } + + /** + * @dataProvider provideAssertExceptionExpectations + */ + public function testInvalidAssertionTypeExceptions(Type\TypeInterface $type, mixed $data, string $expectedMessage): void + { + try { + $type->assert($data); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\AssertException::class)); + } catch (Type\Exception\AssertException $e) { + static::assertSame($expectedMessage, $e->getMessage()); + } + } + + /** + * @dataProvider provideCoerceExceptionExpectations + */ + public function testInvalidCoercionTypeExceptions(Type\TypeInterface $type, mixed $data, string $expectedMessage): void + { + try { + $type->coerce($data); + static::fail(Str\format('Expected "%s" exception to be thrown.', Type\Exception\CoercionException::class)); + } catch (Type\Exception\CoercionException $e) { + static::assertSame($expectedMessage, $e->getMessage()); + } + } +}