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/OutOfBoundsException.php b/src/Psl/Collection/Exception/OutOfBoundsException.php index 94c8630c..fa4b2554 100644 --- a/src/Psl/Collection/Exception/OutOfBoundsException.php +++ b/src/Psl/Collection/Exception/OutOfBoundsException.php @@ -12,7 +12,7 @@ final class OutOfBoundsException extends Exception\OutOfBoundsException implemen /** * @psalm-mutation-free */ - public static function for(string|int $offset): OutOfBoundsException + public static function for(int|string $offset): OutOfBoundsException { return new self(Str\format('Key (%s) was out-of-bounds.', $offset)); } diff --git a/src/Psl/Collection/Exception/RuntimeException.php b/src/Psl/Collection/Exception/RuntimeException.php new file mode 100644 index 00000000..c0562c8e --- /dev/null +++ b/src/Psl/Collection/Exception/RuntimeException.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..3e963f72 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 that you are able remove its values. * * 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,55 @@ 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\RuntimeException 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\RuntimeException If the offset type is not valid. + * + * @return Tv|null The value at the specified offset, null if the offset does not exist. + * + * @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\RuntimeException 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\RuntimeException 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..72729bbd 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,96 @@ 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\RuntimeException 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\RuntimeException('Invalid offset type.'); + } + + return array_key_exists($offset, $this->elements); + } + + /** + * Returns the value at the specified offset. + * + * @param mixed $offset The offset to retrieve. + * + * @throws Exception\RuntimeException If the offset type is not valid. + * + * @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\RuntimeException('Invalid offset type.'); + } + + /** + * @psalm-suppress PossiblyInvalidArrayOffset + */ + return $this->elements[$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\RuntimeException 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\RuntimeException('Invalid offset type.'); + } + + /** + * @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->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\RuntimeException If the offset type is not valid. + */ + public function offsetUnset(mixed $offset): void + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\RuntimeException('Invalid offset type.'); + } + + unset($this->elements[$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..8a787389 --- /dev/null +++ b/src/Psl/Collection/MutableSet.php @@ -0,0 +1,694 @@ + + */ +final class MutableSet implements MutableSetInterface +{ + /** + * @var array + */ + private array $elements = []; + + /** + * MutableSet constructor. + * + * @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 $elements array. + * + * @template Ts of array-key + * + * @param array $elements + * + * @return MutableSet + * + * @pure + */ + public static function fromArray(array $elements): MutableSet + { + return new self($elements); + } + + /** + * 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 $this->firstKey(); + } + + /** + * 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 $this->lastKey(); + } + + /** + * 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 value at the specified key in the current `MutableSet`. + * + * @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 key is in the current `MutableSet`. + * + * @param T $k + * + * @psalm-mutation-free + */ + public function contains(int|string $k): bool + { + return array_key_exists($k, $this->elements); + } + + /** + * Returns the value at the specified key in the current `MutableSet`. + * + * @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`. + * + * @return T|null The first key in the current `MutableSet`, or `null` if the + * current `MutableSet` is empty. + * + * @psalm-mutation-free + */ + public function firstKey(): null|int|string + { + return array_key_first($this->elements); + } + + /** + * Returns the last key in the current `MutableSet`. + * + * @return T|null The last key in the current `MutableSet`, or `null` if the + * current `MutableSet` is empty. + * + * @psalm-mutation-free + */ + public function lastKey(): null|int|string + { + return array_key_last($this->elements); + } + + /** + * Returns the index of the first element that matches the search value. + * + * If no element matches the search value, this function returns null. + * + * @param T $search_value The value that will be search for in the current + * collection. + * + * @return T|null The key (index) where that value is found; null if it is not found. + * + * @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; + } + + /** + * Removes the specified key (and associated value) from the current + * set. + * + * If the key is not in the current set, the current set is + * unchanged. + * + * This will cause elements with higher keys to be assigned a new key that is one less + * than their previous key. + * + * That is, values with keys $k + 1 to n - 1 will be given new keys $k to n - 2, where n is + * the length of the current MutableSet before the call to remove(). + * + * @param T $k The key to remove. + * + * @return MutableSet returns itself. + * + * @psalm-external-mutation-free + */ + public function remove(int|string $k): MutableSet + { + if ($this->contains($k)) { + 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); + } + + /** + * Returns a `MutableVector` containing the keys of the current `MutableSet`. + * + * @return MutableVector + * + * @psalm-mutation-free + */ + public function keys(): MutableVector + { + // there is not need to use `array_keys` here, as the keys are the same as the values. + 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($this->elements, $fn)); + } + + /** + * Returns a `MutableSet` containing the values of the current `MutableSet` + * 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()`. + * + * The keys associated with the current `MutableSet` remain unchanged in the + * returned `MutableSet`; the keys will be used in the filtering process only. + * + * @param (Closure(T, T): bool) $fn The callback containing the condition to apply to the current + * `MutableSet` keys and values. + * + * @return MutableSet A `MutableSet` containing the values after a user-specified + * condition is applied to the keys and values of the current `MutableSet`. + */ + public function filterWithKey(Closure $fn): MutableSet + { + return new MutableSet(Dict\filter_with_key($this->elements, $fn)); + } + + /** + * 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 key/value pairs after a user-specified + * operation is applied. + */ + public function map(Closure $fn): MutableSet + { + return new MutableSet(Dict\map($this->elements, $fn)); + } + + /** + * Returns a `MutableSet` after an operation has been applied to each key and + * value in the current `MutableSet`. + * + * Every key and value in the current `MutableSet` 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 `MutableSet` to the returned + * `MutableSet`. The keys are only used to help in the mapping operation. + * + * @template Tu of array-key + * + * @param (Closure(T, T): Tu) $fn The callback containing the operation to apply to the current + * `MutableSet` keys and values + * + * @return MutableSet A `MutableSet` containing the values after a user-specified + * operation on the current `MutableSet`'s keys and values is applied. + */ + public function mapWithKey(Closure $fn): MutableSet + { + return new MutableSet(Dict\map_with_key($this->elements, $fn)); + } + + /** + * 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 key up + * to, but not including, the element at the provided length from the starting + * key. + * + * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the + * elements at key 0 and 1. + * + * The returned `MutableSet` will always be a proper subset of this + * `MutableSet`. + * + * @param int<0, max> $start The starting key of this set to begin the returned + * `MutableSet`. + * @param null|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\RuntimeException 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\RuntimeException('Invalid offset type.'); + } + + return array_key_exists($offset, $this->elements); + } + + /** + * Returns the value at the specified offset. + * + * @param mixed $offset The offset to retrieve. + * + * @throws Exception\RuntimeException If the offset type is not array-key. + * + * @return T|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\RuntimeException('Invalid offset type.'); + } + + return $this->elements[$offset] ?? null; + } + + /** + * 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\RuntimeException 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\RuntimeException('Invalid offset type.'); + } + + /** + * 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\RuntimeException If the offset type is not valid. + */ + public function offsetUnset(mixed $offset): void + { + if (!is_int($offset) && !is_string($offset)) { + throw new Exception\RuntimeException('Invalid offset type.'); + } + + /** + * @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..da5c59c2 --- /dev/null +++ b/src/Psl/Collection/MutableSetInterface.php @@ -0,0 +1,369 @@ + + * @extends MutableAccessibleCollectionInterface + */ +interface MutableSetInterface extends MutableAccessibleCollectionInterface, SetInterface +{ + /** + * Returns the value at the specified key in the current set. + * + * @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; + + /** + * Returns the value at the specified key in the current set. + * + * @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; + + /** + * Returns a `MutableVectorInterface` containing the keys of the current `MutableSetInterface`. + * + * @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()`. + * + * The keys associated with the current `MutableSetInterface` remain unchanged in the + * returned `MutableSetInterface`. + * + * @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; + + /** + * Returns a `MutableSetInterface` containing the values of the current `MutableSetInterface` + * 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()`. + * + * The keys associated with the current `MutableSetInterface` remain unchanged in the + * returned `MutableSetInterface`; the keys will be used in the filtering process only. + * + * @param (Closure(T, T): bool) $fn The callback containing the condition to apply to the current + * `MutableSetInterface` keys and values. + * + * @return MutableSetInterface A `MutableSetInterface` containing the values after a user-specified + * condition is applied to the keys and values of the current + * `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. + * + * The keys will remain unchanged from the current `MutableSetInterface` to the + * returned `MutableSetInterface`. + * + * @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 key/value pairs after + * a user-specified operation is applied. + */ + public function map(Closure $fn): MutableSetInterface; + + /** + * Returns a `MutableSetInterface` after an operation has been applied to each key and + * value in the current `MutableSetInterface`. + * + * Every key and value in the current `MutableSetInterface` 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 `MutableSetInterface` to the returned + * `MutableSetInterface`. The keys are only used to help in the mapping operation. + * + * @template Tu of array-key + * + * @param (Closure(T, T): Tu) $fn The callback containing the operation to apply to the current + * `MutableSetInterface` keys and values + * + * @return MutableSetInterface A `MutableSetInterface` containing the values after + * a user-specified operation on the current `MutableSetInterface`'s + * keys and values is applied. + */ + 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`. + * + * @return T|null The first key 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`. + * + * @return T|null The last key in the current `MutableSetInterface`, or `null` if the + * current `MutableSetInterface` is empty. + * + * @psalm-mutation-free + */ + public function lastKey(): null|int|string; + + /** + * Returns the index of the first element that matches the search value. + * + * If no element matches the search value, this function returns null. + * + * @param T $search_value The value that will be search for in the current + * `MutableSetInterface`. + * + * @return T|null The key (index) where that value is found; null if it is not found. + * + * @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 key up + * to, but not including, the element at the provided length from the starting + * key. + * + * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the + * elements at key 0 and 1. + * + * The returned `MutableSetInterface` will always be a proper subset of this + * `MutableSetInterface`. + * + * @param int<0, max> $start The starting key of this set to begin the returned + * `MutableSetInterface`. + * @param null|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 key (and associated value) from the current + * set. + * + * If the key is not in the current set, the current set is + * unchanged. + * + * This will cause elements with higher keys to be assigned a new key that is one less + * than their previous key. + * + * That is, values with keys $k + 1 to n - 1 will be given new keys $k to n - 2, where n is + * the length of the current MutableSetInterface before the call to remove(). + * + * If $k is negative, or $k is greater than the largest key in the current set, no changes are made. + * + * @param T $k The key 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..a34991dc 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,97 @@ 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\RuntimeException 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\RuntimeException('Invalid offset type.'); + } + + return array_key_exists($offset, $this->elements); + } + + /** + * Returns the value at the specified offset. + * + * @param mixed $offset The offset to retrieve. + * + * @throws Exception\RuntimeException If the offset type is not a positive integer. + * + * @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\RuntimeException('Invalid offset type.'); + } + + return $this->elements[$offset] ?? null; + } + + /** + * 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\RuntimeException 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\RuntimeException('Invalid offset type.'); + } + + $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\RuntimeException If the offset type is not valid. + */ + public function offsetUnset(mixed $offset): void + { + if (!is_int($offset) || $offset < 0) { + throw new Exception\RuntimeException('Invalid offset type.'); + } + + $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..1ab0514c --- /dev/null +++ b/src/Psl/Collection/Set.php @@ -0,0 +1,521 @@ + + */ +final readonly class Set implements SetInterface +{ + /** + * @var array $elements + */ + private array $elements; + + /** + * @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 $elements array. + * + * @template Ts of array-key + * + * @param array $elements + * + * @return Set + * + * @pure + */ + public static function fromArray(array $elements): Set + { + return new self($elements); + } + + /** + * 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 $this->firstKey(); + } + + /** + * 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 $this->lastKey(); + } + + /** + * 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 value at the specified key in the current `Set`. + * + * @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 key is in the current `Set`. + * + * @param T $k + * + * @psalm-mutation-free + */ + public function contains(int|string $k): bool + { + return array_key_exists($k, $this->elements); + } + + /** + * Returns the value at the specified key in the current `Set`. + * + * @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`. + * + * @return T|null The first key in the current `Set`, or `null` if the + * current `Set` is empty. + * + * @psalm-mutation-free + */ + public function firstKey(): null|int|string + { + return array_key_first($this->elements); + } + + /** + * Returns the last key in the current `Set`. + * + * @return T|null The last key in the current `Set`, or `null` if the + * current `Set` is empty. + * + * @psalm-mutation-free + */ + public function lastKey(): null|int|string + { + return array_key_last($this->elements); + } + + /** + * Returns the index of the first element that matches the search value. + * + * If no element matches the search value, this function returns null. + * + * @param T $search_value The value that will be search for in the current + * collection. + * + * @return T|null The key (index) where that value is found; null if it is not found. + * + * @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); + } + + /** + * Returns a `Vector` containing the keys of the current `Set`. + * + * @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()`. + * + * The keys associated with the current `Set` remain unchanged in the + * returned `Set`. + * + * @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($this->elements, $fn)); + } + + /** + * Returns a `Set` containing the values of the current `Set` + * 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()`. + * + * The keys associated with the current `Set` remain unchanged in the + * returned `Set`; the keys will be used in the filtering process only. + * + * @param (Closure(T, T): bool) $fn The callback containing the condition to apply to the current + * `Set` keys and values. + * + * @return Set a `Set` containing the values after a user-specified + * condition is applied to the keys and values of the current `Set`. + */ + public function filterWithKey(Closure $fn): Set + { + return new Set(Dict\filter_with_key($this->elements, $fn)); + } + + /** + * 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. + * + * The keys will remain unchanged from the current `Set` to the + * returned `Set`. + * + * @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)); + } + + /** + * Returns a `Set` after an operation has been applied to each key and + * value in the current `Set`. + * + * Every key and value in the current `Set` 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 `Set` to the returned + * `Set`. The keys are only used to help in the mapping operation. + * + * @template Tu of array-key + * + * @param (Closure(T, T): Tu) $fn The callback containing the operation to apply to the current + * `Set` keys and values. + * + * @return Set a `Set` containing the values after a user-specified + * operation on the current `Set`'s keys and values is applied. + */ + public function mapWithKey(Closure $fn): Set + { + return new Set(Dict\map_with_key($this->elements, $fn)); + } + + /** + * 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 key up + * to, but not including, the element at the provided length from the starting + * key. + * + * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the + * elements at key 0 and 1. + * + * The returned `Set` will always be a proper subset of this + * `Set`. + * + * @param int<0, max> $start The starting key of this Set to begin the returned + * `Set`. + * @param null|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..8925b53e --- /dev/null +++ b/src/Psl/Collection/SetInterface.php @@ -0,0 +1,329 @@ + + */ +interface SetInterface extends AccessibleCollectionInterface +{ + /** + * Returns the value at the specified key in the current set. + * + * @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 key is in the current set. + * + * @param T $k + * + * @psalm-mutation-free + */ + public function contains(int|string $k): bool; + + /** + * Returns the value at the specified key in the current set. + * + * @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; + + /** + * Returns a `VectorInterface` containing the keys of the current `SetInterface`. + * + * @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()`. + * + * The keys associated with the current `SetInterface` remain unchanged in the + * returned `SetInterface`. + * + * @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; + + /** + * Returns a `SetInterface` containing the values of the current `SetInterface` + * 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()`. + * + * The keys associated with the current `SetInterface` remain unchanged in the + * returned `SetInterface`; the keys will be used in the filtering process only. + * + * @param (Closure(T, T): bool) $fn The callback containing the condition to apply to the current + * `SetInterface` keys and values. + * + * @return SetInterface A `SetInterface` containing the values after a user-specified + * condition is applied to the keys and values of the current `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. + * + * The keys will remain unchanged from the current `SetInterface` to the + * returned `SetInterface`. + * + * @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; + + /** + * Returns a `SetInterface` after an operation has been applied to each key and + * value in the current `SetInterface`. + * + * Every key and value in the current `SetInterface` 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 `SetInterface` to the returned + * `SetInterface`. The keys are only used to help in the mapping operation. + * + * @template Tu of array-key + * + * @param (Closure(T, T): Tu) $fn The callback containing the operation to apply to the current + * `SetInterface` keys and values. + * + * @return SetInterface A `SetInterface` containing the values after a user-specified + * operation on the current `SetInterface`'s keys and values is applied. + */ + 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`. + * + * @return T|null The first key 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`. + * + * @return T|null The last key in the current `SetInterface`, or `null` if the + * current `SetInterface` is empty. + * + * @psalm-mutation-free + */ + public function lastKey(): null|int|string; + + /** + * Returns the index of the first element that matches the search value. + * + * If no element matches the search value, this function returns null. + * + * @param T $search_value The value that will be search for in the current + * `SetInterface`. + * + * @return T|null The key (index) where that value is found; null if it is not found + * + * @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 key up + * to, but not including, the element at the provided length from the starting + * key. + * + * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the + * elements at key 0 and 1. + * + * The returned `SetInterface` will always be a proper subset of this + * `SetInterface`. + * + * @param int<0, max> $start The starting key 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..b894da12 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', diff --git a/src/Psl/Type/Internal/MutableSetType.php b/src/Psl/Type/Internal/MutableSetType.php new file mode 100644 index 00000000..bb8c82d4 --- /dev/null +++ b/src/Psl/Type/Internal/MutableSetType.php @@ -0,0 +1,120 @@ +> + * + * @internal + */ +final readonly class MutableSetType extends Type\Type +{ + /** + * @psalm-mutation-free + * + * @param Type\TypeInterface $key_type + */ + public function __construct( + private readonly Type\TypeInterface $key_type, + ) { + } + + /** + * @throws CoercionException + * + * @return Collection\MutableSetInterface + */ + public function coerce(mixed $value): Collection\MutableSetInterface + { + if (is_iterable($value)) { + /** @var Type\Type $key_type */ + $key_type = $this->key_type; + /** @var list $keys */ + $keys = []; + $key = null; + $iterating = true; + try { + /** + * @var T $key + */ + foreach ($value as $key => $_) { + $iterating = false; + $keys[] = $key_type->coerce($key); + $iterating = true; + } + } catch (Throwable $e) { + throw match (true) { + $iterating => CoercionException::withValue(null, $this->toString(), PathExpression::iteratorError($key), $e), + default => CoercionException::withValue($key, $this->toString(), PathExpression::iteratorKey($key), $e), + }; + } + + /** @var Collection\MutableSet */ + return new Collection\MutableSet($keys); + } + + 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 $key_type */ + $key_type = $this->key_type; + /** @var list $keys */ + $keys = []; + $key = null; + $iterating = true; + try { + /** + * @var T $key + */ + foreach ($value as $key => $_) { + $iterating = false; + $keys[] = $key_type->assert($key); + $iterating = true; + } + } catch (Throwable $e) { + throw match (true) { + $iterating => AssertException::withValue(null, $this->toString(), PathExpression::iteratorError($key), $e), + default => AssertException::withValue($key, $this->toString(), PathExpression::iteratorKey($key), $e), + }; + } + + /** @var Collection\MutableSet */ + return new Collection\MutableSet($keys); + } + + throw AssertException::withValue($value, $this->toString()); + } + + public function toString(): string + { + return Str\format( + '%s<%s>', + Collection\SetInterface::class, + $this->key_type->toString(), + ); + } +} diff --git a/src/Psl/Type/Internal/SetType.php b/src/Psl/Type/Internal/SetType.php new file mode 100644 index 00000000..2621173b --- /dev/null +++ b/src/Psl/Type/Internal/SetType.php @@ -0,0 +1,120 @@ +> + * + * @internal + */ +final readonly class SetType extends Type\Type +{ + /** + * @psalm-mutation-free + * + * @param Type\TypeInterface $key_type + */ + public function __construct( + private readonly Type\TypeInterface $key_type, + ) { + } + + /** + * @throws CoercionException + * + * @return Collection\SetInterface + */ + public function coerce(mixed $value): Collection\SetInterface + { + if (is_iterable($value)) { + /** @var Type\Type $key_type */ + $key_type = $this->key_type; + /** @var list $keys */ + $keys = []; + $key = null; + $iterating = true; + try { + /** + * @var T $key + */ + foreach ($value as $key => $_) { + $iterating = false; + $keys[] = $key_type->coerce($key); + $iterating = true; + } + } catch (Throwable $e) { + throw match (true) { + $iterating => CoercionException::withValue(null, $this->toString(), PathExpression::iteratorError($key), $e), + default => CoercionException::withValue($key, $this->toString(), PathExpression::iteratorKey($key), $e), + }; + } + + /** @var Collection\Set */ + return new Collection\Set($keys); + } + + 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 $key_type */ + $key_type = $this->key_type; + /** @var list $keys */ + $keys = []; + $key = null; + $iterating = true; + try { + /** + * @var T $key + */ + foreach ($value as $key => $_) { + $iterating = false; + $keys[] = $key_type->assert($key); + $iterating = true; + } + } catch (Throwable $e) { + throw match (true) { + $iterating => AssertException::withValue(null, $this->toString(), PathExpression::iteratorError($key), $e), + default => AssertException::withValue($key, $this->toString(), PathExpression::iteratorKey($key), $e), + }; + } + + /** @var Collection\Set */ + return new Collection\Set($keys); + } + + throw AssertException::withValue($value, $this->toString()); + } + + public function toString(): string + { + return Str\format( + '%s<%s>', + Collection\SetInterface::class, + $this->key_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); +}