diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 779f162..2f3eecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,11 @@ on: [push, pull_request] jobs: blackbox: - uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/black-box-matrix.yml@main coverage: - uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/coverage-matrix.yml@main secrets: inherit psalm: - uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@next + uses: innmind/github-workflows/.github/workflows/psalm-matrix.yml@main cs: - uses: innmind/github-workflows/.github/workflows/cs.yml@next + uses: innmind/github-workflows/.github/workflows/cs.yml@main diff --git a/README.md b/README.md index 3af62f3..2bd64a3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# mutable +# Mutable -[![Build Status](https://github.com/innmind/mutable/workflows/CI/badge.svg?branch=main)](https://github.com/innmind/mutable/actions?query=workflow%3ACI) +[![Build Status](https://github.com/Innmind/mutable/actions/workflows/ci.yml/badge.svg)](https://github.com/Innmind/mutable/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/innmind/mutable/branch/develop/graph/badge.svg)](https://codecov.io/gh/innmind/mutable) [![Type Coverage](https://shepherd.dev/github/innmind/mutable/coverage.svg)](https://shepherd.dev/github/innmind/mutable) -Description +This a collection of mutable data structures. ## Installation @@ -14,4 +14,10 @@ composer require innmind/mutable ## Usage -Todo +Available structures: + +- `Innmind\Mutable\Map` +- `Innmind\Mutable\Queue` FIFO queue +- `Innmind\Mutable\Ring` Circle through a fixed sequence of data in an infinite loop +- `Innmind\Mutable\Set` +- `Innmind\Mutable\Stack` LIFO queue diff --git a/blackbox.php b/blackbox.php index ff9d705..2588bdc 100644 --- a/blackbox.php +++ b/blackbox.php @@ -11,7 +11,7 @@ Application::new($argv) ->when( - \get_env('ENABLE_COVERAGE') !== false, + \getenv('ENABLE_COVERAGE') !== false, static fn(Application $app) => $app ->scenariiPerProof(1) ->codeCoverage( diff --git a/composer.json b/composer.json index d797ac8..96ccac4 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "issues": "http://github.com/innmind/mutable/issues" }, "require": { - "php": "~8.4" + "php": "~8.4", + "innmind/immutable": "~6.0" }, "autoload": { "psr-4": { @@ -23,7 +24,7 @@ } }, "require-dev": { - "innmind/static-analysis": "^1.2.1", + "innmind/static-analysis": "~1.3", "innmind/black-box": "~6.5", "innmind/coding-standard": "~2.0" } diff --git a/proofs/.gitkeep b/proofs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/proofs/queue.php b/proofs/queue.php new file mode 100644 index 0000000..973c1c3 --- /dev/null +++ b/proofs/queue.php @@ -0,0 +1,43 @@ +same(0, $queue->size()); + $assert->true($queue->empty()); + + foreach ($values as $i => $value) { + $queue->push($value); + + $assert->false($queue->empty()); + $assert->same($i + 1, $queue->size()); + } + + $pulled = []; + $size = $queue->size(); + + foreach ($values as $i => $_) { + $pulled[] = $queue->pull()->match( + static fn($value) => $value, + static fn() => null, + ); + + $assert->same($size - 1, $queue->size()); + $size = $queue->size(); + } + + $assert->true($queue->empty()); + $assert->same($values, $pulled); + }, + ); +}; diff --git a/proofs/ring.php b/proofs/ring.php new file mode 100644 index 0000000..0329669 --- /dev/null +++ b/proofs/ring.php @@ -0,0 +1,79 @@ +atLeast(1), + Set::integers()->between(1, 10), + ), + static function($assert, $values, $rotations) { + $ring = Ring::of(...$values); + $pulled = []; + + foreach (\range(1, $rotations) as $_) { + foreach (\range(1, \count($values)) as $__) { + $pulled[] = $ring->pull()->match( + static fn($value) => $value, + static fn() => null, + ); + } + } + + $expected = \array_merge( + ...\array_fill( + 0, + $rotations, + $values, + ), + ); + + $assert->same($expected, $pulled); + }, + ); + + yield proof( + 'Partial Ring rotations', + given( + Set::sequence(Set::type())->atLeast(1), + Set::integers()->between(1, 1_000), + ), + static function($assert, $values, $toPull) { + $ring = Ring::of(...$values); + $pulled = []; + + foreach (\range(1, $toPull) as $_) { + $pulled[] = $ring->pull()->match( + static fn($value) => $value, + static fn() => null, + ); + } + + $expected = \array_slice( + \array_merge( + ...\array_fill( + 0, + (int) \ceil($toPull / \count($values)), + $values, + ), + ), + 0, + $toPull, + ); + + $assert->same($expected, $pulled); + }, + ); + + yield test( + 'Empty Ring returns nothing', + static fn($assert) => $assert->false(Ring::of()->pull()->match( + static fn() => true, + static fn() => false, + )), + ); +}; diff --git a/proofs/stack.php b/proofs/stack.php new file mode 100644 index 0000000..2843c85 --- /dev/null +++ b/proofs/stack.php @@ -0,0 +1,43 @@ +same(0, $stack->size()); + $assert->true($stack->empty()); + + foreach ($values as $i => $value) { + $stack->push($value); + + $assert->false($stack->empty()); + $assert->same($i + 1, $stack->size()); + } + + $pulled = []; + $size = $stack->size(); + + foreach ($values as $i => $_) { + $pulled[] = $stack->pull()->match( + static fn($value) => $value, + static fn() => null, + ); + + $assert->same($size - 1, $stack->size()); + $size = $stack->size(); + } + + $assert->true($stack->empty()); + $assert->same(\array_reverse($values), $pulled); + }, + ); +}; diff --git a/psalm.xml b/psalm.xml index 5d768ff..a270b9b 100644 --- a/psalm.xml +++ b/psalm.xml @@ -14,4 +14,7 @@ + + + diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Map.php b/src/Map.php new file mode 100644 index 0000000..70df0a1 --- /dev/null +++ b/src/Map.php @@ -0,0 +1,159 @@ + $map + */ + private function __construct( + private Immutable\Map $map, + ) { + } + + /** + * Set a new key/value pair + * + * @param K $key + * @param V $value + */ + public function __invoke(mixed $key, mixed $value): void + { + $this->map = ($this->map)($key, $value); + } + + /** + * @template A + * @template B + * @no-named-arguments + * @psalm-pure + * + * @param list $pairs + * + * @return self + */ + public static function of(array ...$pairs): self + { + return new self(Immutable\Map::of(...$pairs)); + } + + /** + * @return int<0, max> + */ + #[\NoDiscard] + public function size(): int + { + return $this->map->size(); + } + + /** + * Set a new key/value pair + * + * @param K $key + * @param V $value + */ + public function put(mixed $key, mixed $value): void + { + $this->map = ($this->map)($key, $value); + } + + /** + * Return the element with the given key + * + * @param K $key + * + * @return Maybe + */ + #[\NoDiscard] + public function get(mixed $key): Maybe + { + return $this->map->get($key); + } + + /** + * Check if there is an element for the given key + * + * @param K $key + */ + #[\NoDiscard] + public function contains(mixed $key): bool + { + return $this->map->contains($key); + } + + /** + * Remove all elements from the map + */ + public function clear(): void + { + $this->map = $this->map->clear(); + } + + /** + * Remove all elements that don't match the predicate + * + * @param callable(K, V): bool $predicate + */ + public function filter(callable $predicate): void + { + $this->map = $this->map->filter($predicate); + } + + /** + * Remove all elements that match the predicate + * + * @param callable(K, V): bool $predicate + */ + public function exclude(callable $predicate): void + { + $this->map = $this->map->exclude($predicate); + } + + /** + * Run the given function for each element of the map + * + * @param callable(K, V): void $function + */ + public function foreach(callable $function): void + { + $_ = $this->map->foreach($function); + } + + /** + * Remove the element with the given key + * + * @param K $key + */ + public function remove(mixed $key): void + { + $this->map = $this->map->remove($key); + } + + #[\NoDiscard] + public function empty(): bool + { + return $this->map->empty(); + } + + /** + * @return Immutable\Map + */ + #[\NoDiscard] + public function snapshot(): Immutable\Map + { + return $this->map; + } +} diff --git a/src/Queue.php b/src/Queue.php new file mode 100644 index 0000000..9a1df4a --- /dev/null +++ b/src/Queue.php @@ -0,0 +1,82 @@ + $data + */ + private function __construct( + private Sequence $data, + ) { + } + + /** + * @template A + * @no-named-arguments + * @psalm-pure + * + * @param A ...$values + * + * @return self + */ + public static function of(mixed ...$values): self + { + return new self(Sequence::of(...$values)); + } + + /** + * @param T $element + */ + public function push(mixed $element): void + { + $this->data = ($this->data)($element); + } + + /** + * @return Maybe + */ + #[\NoDiscard] + public function pull(): Maybe + { + $value = $this->data->first(); + $this->data = $this->data->drop(1); + + return $value; + } + + /** + * @return int<0, max> + */ + #[\NoDiscard] + public function size(): int + { + return $this->data->size(); + } + + #[\NoDiscard] + public function empty(): bool + { + return $this->data->empty(); + } + + /** + * Remove all elements from the queue + */ + public function clear(): void + { + $this->data = $this->data->clear(); + } +} diff --git a/src/Ring.php b/src/Ring.php new file mode 100644 index 0000000..a909b44 --- /dev/null +++ b/src/Ring.php @@ -0,0 +1,64 @@ + $data + */ + private function __construct( + private array $data, + ) { + } + + /** + * @template A + * @no-named-arguments + * @psalm-pure + * + * @param A ...$values + * + * @return self + */ + public static function of(mixed ...$values): self + { + return new self($values); + } + + /** + * @return Maybe + */ + public function pull(): Maybe + { + if (!\array_key_exists(0, $this->data)) { + /** @var Maybe */ + return Maybe::nothing(); + } + + $value = Maybe::just(\current($this->data)); + \next($this->data); + + if (\is_null(\key($this->data))) { + \reset($this->data); + } + + return $value; + } + + /** + * Move the ring cursor to the first value + */ + public function reset(): void + { + \reset($this->data); + } +} diff --git a/src/Set.php b/src/Set.php new file mode 100644 index 0000000..7f2cd49 --- /dev/null +++ b/src/Set.php @@ -0,0 +1,139 @@ + $set + */ + private function __construct( + private Immutable\Set $set, + ) { + } + + /** + * Add an element to the set + * + * @param T $element + */ + public function __invoke(mixed $element): void + { + $this->set = ($this->set)($element); + } + + /** + * @template A + * @no-named-arguments + * @psalm-pure + * + * @param A $values + * + * @return self + */ + public static function of(mixed ...$values): self + { + return new self(Immutable\Set::of(...$values)); + } + + /** + * @return int<0, max> + */ + #[\NoDiscard] + public function size(): int + { + return $this->set->size(); + } + + /** + * Add an element to the set + * + * @param T $element + */ + public function add(mixed $element): void + { + $this->set = ($this->set)($element); + } + + /** + * Check if the set contains the given element + * + * @param T $element + */ + #[\NoDiscard] + public function contains(mixed $element): bool + { + return $this->set->contains($element); + } + + /** + * Remove the element from the set + * + * @param T $element + */ + public function remove(mixed $element): void + { + $this->set = $this->set->remove($element); + } + + /** + * Remove all elements that don't satisfy the given predicate + * + * @param callable(T): bool $predicate + */ + public function filter(callable $predicate): void + { + $this->set = $this->set->filter($predicate); + } + + /** + * Remove all elements that satisfy the given predicate + * + * @param callable(T): bool $predicate + */ + public function exclude(callable $predicate): void + { + $this->set = $this->set->exclude($predicate); + } + + /** + * Apply the given function to all elements of the set + * + * @param callable(T): void $function + */ + public function foreach(callable $function): void + { + $_ = $this->set->foreach($function); + } + + /** + * Removes all elements from the set + */ + public function clear(): void + { + $this->set = $this->set->clear(); + } + + #[\NoDiscard] + public function empty(): bool + { + return $this->set->empty(); + } + + /** + * @return Immutable\Set + */ + #[\NoDiscard] + public function snapshot(): Immutable\Set + { + return $this->set; + } +} diff --git a/src/Stack.php b/src/Stack.php new file mode 100644 index 0000000..f86c75d --- /dev/null +++ b/src/Stack.php @@ -0,0 +1,82 @@ + $data + */ + private function __construct( + private Sequence $data, + ) { + } + + /** + * @template A + * @no-named-arguments + * @psalm-pure + * + * @param A ...$values + * + * @return self + */ + public static function of(mixed ...$values): self + { + return new self(Sequence::of(...$values)); + } + + /** + * @param T $element + */ + public function push(mixed $element): void + { + $this->data = ($this->data)($element); + } + + /** + * @return Maybe + */ + #[\NoDiscard] + public function pull(): Maybe + { + $value = $this->data->last(); + $this->data = $this->data->dropEnd(1); + + return $value; + } + + /** + * @return int<0, max> + */ + #[\NoDiscard] + public function size(): int + { + return $this->data->size(); + } + + #[\NoDiscard] + public function empty(): bool + { + return $this->data->empty(); + } + + /** + * Remove all elements from the queue + */ + public function clear(): void + { + $this->data = $this->data->clear(); + } +}