From ed1549b87434c2758a4f9c4dc65059d0767ac398 Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Wed, 2 Oct 2024 17:11:29 -0300 Subject: [PATCH] feat: Implements contains operation for Collection. (#10) --- .gitattributes | 13 +++ README.md | 106 ++++++++++-------- composer.json | 14 ++- phpstan.neon.dist | 14 +++ src/Collectible.php | 30 +++-- src/Collection.php | 8 +- src/Internal/Iterators/InternalIterator.php | 17 +-- src/Internal/Operations/Compare/Contains.php | 32 ++++++ src/Internal/Operations/Filter/Filter.php | 4 +- src/Internal/Operations/LazyOperation.php | 2 +- src/Internal/Operations/Operation.php | 3 - tests/Models/CryptoCurrency.php | 5 +- .../CollectionContainsOperationTest.php | 87 ++++++++++++++ ....php => CollectionEqualsOperationTest.php} | 2 +- 14 files changed, 258 insertions(+), 79 deletions(-) create mode 100644 .gitattributes create mode 100644 phpstan.neon.dist create mode 100644 src/Internal/Operations/Compare/Contains.php create mode 100644 tests/Operations/Compare/CollectionContainsOperationTest.php rename tests/Operations/Compare/{CollectionCompareOperationTest.php => CollectionEqualsOperationTest.php} (97%) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8efe2ca --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +/tests export-ignore +/vendor export-ignore + +/README.md export-ignore +/LICENSE export-ignore +/Makefile export-ignore +/phpmd.xml export-ignore +/phpunit.xml export-ignore +/phpstan.neon.dist export-ignore +/infection.json.dist export-ignore + +/.github export-ignore +/.gitignore export-ignore diff --git a/README.md b/README.md index 57ab530..80b9f5b 100644 --- a/README.md +++ b/README.md @@ -145,42 +145,48 @@ These methods enable filtering elements in the Collection based on specific cond These methods enable sorting elements in the Collection based on the specified order and optional predicates. -- `sort`: Sorts the Collection. -

+#### Sort by order and custom predicate - - **Sort by order**: You can sort the Collection in ascending or descending order based on keys or values. -

- `Order::ASCENDING_KEY`: Sorts the collection in ascending order by key. +- `sort`: Sorts the Collection. - `Order::DESCENDING_KEY`: Sorts the collection in descending order by key. + ``` + Order::ASCENDING_KEY: Sorts the collection in ascending order by key. + Order::DESCENDING_KEY: Sorts the collection in descending order by key. + Order::ASCENDING_VALUE: Sorts the collection in ascending order by value. + Order::DESCENDING_VALUE: Sorts the collection in descending order by value. + ``` - `Order::ASCENDING_VALUE`: Sorts the collection in ascending order by value. + By default, `Order::ASCENDING_KEY` is used. - `Order::DESCENDING_VALUE`: Sorts the collection in descending order by value. -

- By default, `Order::ASCENDING_KEY` is used. + ```php + use TinyBlocks\Collection\Internal\Operations\Order\Order; + + $collection->sort(order: Order::DESCENDING_VALUE); + ``` - ```php - use TinyBlocks\Collection\Internal\Operations\Order\Order; + Sort the Collection using a custom predicate to determine how elements should be + compared. - $collection->sort(order: Order::DESCENDING_VALUE); - ``` + ```php + use TinyBlocks\Collection\Internal\Operations\Order\Order; + + $collection->sort(order: Order::ASCENDING_VALUE, predicate: fn(Amount $amount): float => $amount->value); + ``` - - **Sort by custom predicate**: Sort the Collection using a custom predicate to determine how elements should be - compared. +
- ```php - use TinyBlocks\Collection\Internal\Operations\Order\Order; +### Retrieving - $collection->sort(order: Order::ASCENDING_VALUE, predicate: fn(Amount $amount): float => $amount->value); - ``` +These methods allow access to elements within the Collection, such as fetching the first or last element, counting the +elements, or finding elements that match a specific condition. -
+#### Retrieve count -### Retrieving +- `count`: Returns the total number of elements in the Collection. -These methods allow access to elements within the Collection, such as fetching the first or last element or finding -elements that match a specific condition. + ```php + $collection->count(); + ``` #### Retrieve single elements @@ -216,6 +222,14 @@ elements that match a specific condition. These methods enable comparing collections to check for equality or to apply other comparison logic. +#### Check if collection contains element + +- `contains`: Checks if the Collection contains a specific element. + + ```php + $collection->contains(element: 5); + ``` + #### Compare collections for equality - `equals`: Compares the current Collection with another collection to check if they are equal. @@ -266,34 +280,36 @@ These methods allow the Collection's elements to be transformed or converted int #### Convert to array - `toArray`: Converts the Collection into an array. -

- `PreserveKeys::DISCARD`: Converts while discarding the keys. - `PreserveKeys::PRESERVE`: Converts while preserving the original keys. -

- **By default, `PreserveKeys::PRESERVE` is used.** + ``` + PreserveKeys::DISCARD: Converts while discarding the keys. + PreserveKeys::PRESERVE: Converts while preserving the original keys. + ``` - ```php - use TinyBlocks\Collection\Internal\Operations\Transform\PreserveKeys; + By default, `PreserveKeys::PRESERVE` is used. - $collection->toArray(preserveKeys: PreserveKeys::DISCARD); - ``` + ```php + use TinyBlocks\Collection\Internal\Operations\Transform\PreserveKeys; + + $collection->toArray(preserveKeys: PreserveKeys::DISCARD); + ``` #### Convert to JSON - `toJson`: Converts the Collection into a JSON string. -

- `PreserveKeys::DISCARD`: Converts while discarding the keys. - `PreserveKeys::PRESERVE`: Converts while preserving the original keys. -

- **By default, `PreserveKeys::PRESERVE` is used.** + ``` + PreserveKeys::DISCARD: Converts while discarding the keys. + PreserveKeys::PRESERVE: Converts while preserving the original keys. + ``` - ```php - use TinyBlocks\Collection\Internal\Operations\Transform\PreserveKeys; + By default, `PreserveKeys::PRESERVE` is used. - $collection->toJson(preserveKeys: PreserveKeys::DISCARD); - ``` + ```php + use TinyBlocks\Collection\Internal\Operations\Transform\PreserveKeys; + + $collection->toJson(preserveKeys: PreserveKeys::DISCARD); + ```
@@ -307,18 +323,18 @@ provide lazy evaluation, meaning elements are only generated as needed. It cannot be reused once a generator is consumed (i.e., after you iterate over it or apply certain operations). This behavior is intended to optimize memory usage and performance but can sometimes lead to confusion when reusing an -iterator after operations like `reduce`, `map`, or `filter`. +iterator after operations like `count`, `toJson`, or `toArray`. ### 02. How does lazy evaluation affect memory usage in Collection? Lazy evaluation, enabled by [PHP's Generators](https://www.php.net/manual/en/language.generators.overview.php), allows -Collection to handle large datasets without loading all elements into memory at once. +`Collection` to handle large datasets without loading all elements into memory at once. This results in significant memory savings when working with large datasets or performing complex chained operations. However, this also means that some operations will entirely consume the generator, and you won't be -able to reaccess the elements unless you recreate the Collection. +able to reaccess the elements unless you recreate the `Collection`.
diff --git a/composer.json b/composer.json index bd3ee02..66a8277 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,6 @@ "json", "array", "yield", - "psr-4", - "psr-12", "iterator", "generator", "collection", @@ -24,6 +22,10 @@ "homepage": "https://github.com/gustavofreze" } ], + "support": { + "issues": "https://github.com/tiny-blocks/collection/issues", + "source": "https://github.com/tiny-blocks/collection" + }, "config": { "sort-packages": true, "allow-plugins": { @@ -42,25 +44,27 @@ }, "require": { "php": "^8.2", - "tiny-blocks/serializer": "^3", - "tiny-blocks/value-object": "^2" + "tiny-blocks/serializer": "^3" }, "require-dev": { "phpmd/phpmd": "^2.15", "phpunit/phpunit": "^11", + "phpstan/phpstan": "^1", "infection/infection": "^0.29", "squizlabs/php_codesniffer": "^3.10" }, "scripts": { "phpcs": "phpcs --standard=PSR12 --extensions=php ./src", "phpmd": "phpmd ./src text phpmd.xml --suffixes php --ignore-violations-on-exit", + "phpstan": "phpstan analyse -c phpstan.neon.dist --quiet --no-progress", "test": "phpunit --log-junit=report/coverage/junit.xml --coverage-xml=report/coverage/coverage-xml --coverage-html=report/coverage/coverage-html tests", "test-mutation": "infection --only-covered --logger-html=report/coverage/mutation-report.html --coverage=report/coverage --min-msi=100 --min-covered-msi=100 --threads=4", "test-no-coverage": "phpunit --no-coverage", "test-mutation-no-coverage": "infection --only-covered --min-msi=100 --threads=4", "review": [ "@phpcs", - "@phpmd" + "@phpmd", + "@phpstan" ], "tests": [ "@test", diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..960f73d --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,14 @@ +parameters: + paths: + - src + level: 9 + tmpDir: report/phpstan + ignoreErrors: + - '#Unsafe usage of new static#' + - '#does not specify its types#' + - '#contains incompatible type#' + - '#specified in iterable type iterable#' + - '#is not subtype of native type static#' + - '#PHPDoc tag @extends has invalid value#' + - '#type has no value type specified in iterable type array#' + reportUnmatchedIgnoredErrors: false diff --git a/src/Collectible.php b/src/Collectible.php index b7d59f2..3abf200 100644 --- a/src/Collectible.php +++ b/src/Collectible.php @@ -14,8 +14,10 @@ /** * Represents a collection that can be manipulated, iterated, and counted. * + * @template Key of int|string + * @template Value of mixed * @template Element of mixed - * @extends IteratorAggregate + * @extends IteratorAggregate */ interface Collectible extends Countable, IteratorAggregate { @@ -42,6 +44,21 @@ public static function createFromEmpty(): static; */ public function add(mixed ...$elements): Collectible; + /** + * Checks if the collection contains a specific element. + * + * @param Element $element The element to check for. + * @return bool True if the element is found, false otherwise. + */ + public function contains(mixed $element): bool; + + /** + * Returns the total number of elements in the Collection. + * + * @return int The number of elements in the collection. + */ + public function count(): int; + /** * Executes actions on each element in the collection without modifying it. * @@ -83,13 +100,6 @@ public function findBy(Closure ...$predicates): mixed; */ public function first(mixed $defaultValueIfNotFound = null): mixed; - /** - * Counts the number of elements in the collection. - * - * @return int The number of elements in the collection. - */ - public function count(): int; - /** * Retrieves an element by its index, or a default value if not found. * @@ -102,7 +112,7 @@ public function getBy(int $index, mixed $defaultValueIfNotFound = null): mixed; /** * Returns an iterator for traversing the collection. * - * @return Traversable An iterator for the collection. + * @return Traversable An iterator for the collection. */ public function getIterator(): Traversable; @@ -184,7 +194,7 @@ public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate = * By default, `PreserveKeys::PRESERVE` is used. * * @param PreserveKeys $preserveKeys The option to preserve or discard array keys. - * @return array The resulting array. + * @return array The resulting array. */ public function toArray(PreserveKeys $preserveKeys = PreserveKeys::PRESERVE): array; diff --git a/src/Collection.php b/src/Collection.php index ef456c8..8ab870e 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -7,6 +7,7 @@ use Closure; use TinyBlocks\Collection\Internal\Iterators\InternalIterator; use TinyBlocks\Collection\Internal\Operations\Aggregate\Reduce; +use TinyBlocks\Collection\Internal\Operations\Compare\Contains; use TinyBlocks\Collection\Internal\Operations\Compare\Equals; use TinyBlocks\Collection\Internal\Operations\Filter\Filter; use TinyBlocks\Collection\Internal\Operations\Order\Order; @@ -45,7 +46,7 @@ private function __construct(InternalIterator $iterator) public static function createFrom(iterable $elements): static { - return new static(iterator: InternalIterator::from(elements: $elements, operations: Create::fromEmpty())); + return new static(iterator: InternalIterator::from(elements: $elements, operation: Create::fromEmpty())); } public static function createFromEmpty(): static @@ -58,6 +59,11 @@ public function add(mixed ...$elements): static return new static(iterator: $this->iterator->add(operation: Add::from(newElements: $elements))); } + public function contains(mixed $element): bool + { + return Contains::from(elements: $this->iterator)->exists(element: $element); + } + public function count(): int { return iterator_count($this->iterator); diff --git a/src/Internal/Iterators/InternalIterator.php b/src/Internal/Iterators/InternalIterator.php index f925d15..647051d 100644 --- a/src/Internal/Iterators/InternalIterator.php +++ b/src/Internal/Iterators/InternalIterator.php @@ -12,28 +12,31 @@ * A generator-based iterator that applies operations lazily to collections, * ensuring efficient memory usage by yielding elements on demand. * - * @template Key - * @template Value + * @template Key of int|string + * @template Value of mixed * @implements IteratorAggregate */ final class InternalIterator implements IteratorAggregate { + private array $operations; + /** * @param iterable $elements - * @param iterable $operations + * @param LazyOperation $operation */ - private function __construct(private readonly iterable $elements, private iterable $operations) + private function __construct(private readonly iterable $elements, LazyOperation $operation) { + $this->operations[] = $operation; } /** * @param iterable $elements - * @param LazyOperation ...$operations + * @param LazyOperation $operation * @return InternalIterator */ - public static function from(iterable $elements, LazyOperation ...$operations): InternalIterator + public static function from(iterable $elements, LazyOperation $operation): InternalIterator { - return new InternalIterator(elements: $elements, operations: $operations); + return new InternalIterator(elements: $elements, operation: $operation); } /** diff --git a/src/Internal/Operations/Compare/Contains.php b/src/Internal/Operations/Compare/Contains.php new file mode 100644 index 0000000..9327a8c --- /dev/null +++ b/src/Internal/Operations/Compare/Contains.php @@ -0,0 +1,32 @@ +elements as $current) { + if ($equals->compareWith(element: $current, otherElement: $element)) { + return true; + } + } + + return false; + } +} diff --git a/src/Internal/Operations/Filter/Filter.php b/src/Internal/Operations/Filter/Filter.php index 98a3449..0d2da64 100644 --- a/src/Internal/Operations/Filter/Filter.php +++ b/src/Internal/Operations/Filter/Filter.php @@ -12,12 +12,12 @@ final class Filter implements LazyOperation { private array $predicates; - private function __construct(Closure ...$predicates) + private function __construct(?Closure ...$predicates) { $this->predicates = $predicates; } - public static function from(Closure ...$predicates): Filter + public static function from(?Closure ...$predicates): Filter { return new Filter(...$predicates); } diff --git a/src/Internal/Operations/LazyOperation.php b/src/Internal/Operations/LazyOperation.php index 8868438..a1a3956 100644 --- a/src/Internal/Operations/LazyOperation.php +++ b/src/Internal/Operations/LazyOperation.php @@ -11,7 +11,7 @@ * * @template Key of array-key * @template Value - * @extends Operation + * @extends Operation */ interface LazyOperation extends Operation { diff --git a/src/Internal/Operations/Operation.php b/src/Internal/Operations/Operation.php index 795645f..13b9752 100644 --- a/src/Internal/Operations/Operation.php +++ b/src/Internal/Operations/Operation.php @@ -6,9 +6,6 @@ /** * Defines operations to the collection. - * - * @template Key of array-key - * @template Value */ interface Operation { diff --git a/tests/Models/CryptoCurrency.php b/tests/Models/CryptoCurrency.php index 8f59ce1..cf39eee 100644 --- a/tests/Models/CryptoCurrency.php +++ b/tests/Models/CryptoCurrency.php @@ -6,13 +6,10 @@ use TinyBlocks\Serializer\Serializer; use TinyBlocks\Serializer\SerializerAdapter; -use TinyBlocks\Vo\ValueObject; -use TinyBlocks\Vo\ValueObjectAdapter; -final class CryptoCurrency implements Serializer, ValueObject +final class CryptoCurrency implements Serializer { use SerializerAdapter; - use ValueObjectAdapter; public function __construct(public string $name, public float $price, public string $symbol) { diff --git a/tests/Operations/Compare/CollectionContainsOperationTest.php b/tests/Operations/Compare/CollectionContainsOperationTest.php new file mode 100644 index 0000000..91aac9c --- /dev/null +++ b/tests/Operations/Compare/CollectionContainsOperationTest.php @@ -0,0 +1,87 @@ +contains(element: $element); + + /** @Then the collection should contain the element */ + self::assertTrue($actual); + } + + #[DataProvider('doesNotContainElementDataProvider')] + public function testDoesNotContainElement(iterable $elements, mixed $element): void + { + /** @Given a collection */ + $collection = Collection::createFrom(elements: $elements); + + /** @When checking if the element is contained in the collection */ + $actual = $collection->contains(element: $element); + + /** @Then the collection should not contain the element */ + self::assertFalse($actual); + } + + public static function containsElementDataProvider(): iterable + { + yield 'Collection contains null' => [ + 'elements' => [1, null, 3], + 'element' => null + ]; + + yield 'Collection contains element' => [ + 'elements' => [ + new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'), + new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH') + ], + 'element' => new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC') + ]; + + yield 'Collection contains scalar value' => [ + 'elements' => [1, 'key' => 'value', 3.5], + 'element' => 'value' + ]; + } + + public static function doesNotContainElementDataProvider(): iterable + { + yield 'Empty collection' => [ + 'elements' => [], + 'element' => 1 + ]; + + yield 'Collection does not contain object' => [ + 'elements' => [new stdClass()], + 'element' => new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC') + ]; + + yield 'Collection does not contain element' => [ + 'elements' => [ + new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'), + new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH') + ], + 'element' => new CryptoCurrency(name: 'Ripple', price: 1.0, symbol: 'XRP') + ]; + + yield 'Collection does not contain scalar value' => [ + 'elements' => [1, 'key' => 'value', 3.5], + 'element' => 42 + ]; + } +} diff --git a/tests/Operations/Compare/CollectionCompareOperationTest.php b/tests/Operations/Compare/CollectionEqualsOperationTest.php similarity index 97% rename from tests/Operations/Compare/CollectionCompareOperationTest.php rename to tests/Operations/Compare/CollectionEqualsOperationTest.php index abff5df..0194684 100644 --- a/tests/Operations/Compare/CollectionCompareOperationTest.php +++ b/tests/Operations/Compare/CollectionEqualsOperationTest.php @@ -10,7 +10,7 @@ use TinyBlocks\Collection\Collection; use TinyBlocks\Collection\Models\CryptoCurrency; -final class CollectionCompareOperationTest extends TestCase +final class CollectionEqualsOperationTest extends TestCase { #[DataProvider('collectionsEqualDataProvider')] public function testCollectionsAreEqual(iterable $elementsA, iterable $elementsB): void