Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implements reduce function for aggregating values in collections. #5

Merged
merged 1 commit into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* [Ordering](#ordering)
* [Retrieving](#retrieving)
* [Comparing](#comparing)
* [Aggregation](#aggregation)
* [Transforming](#transforming)
* [License](#license)
* [Contributing](#contributing)
Expand Down Expand Up @@ -208,6 +209,19 @@ These methods enable comparing collections to check for equality or to apply oth

<div id='retrieving'></div>

### Aggregation

These methods perform operations that return a single value based on the collection's content, such as summing or
combining elements.

- `reduce`: Combines all elements in the collection into a single value using the provided aggregator function and an
initial value.
This method is useful for accumulating results, like summing or concatenating values.

```php
$collection->reduce(aggregator: fn(float $carry, float $amount): float => $carry + $amount, initial: 0.0)
```

### Transforming

These methods allow the collection's elements to be transformed or converted into different formats.
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"yield",
"psr-4",
"psr-12",
"iterator",
"generator",
"collection",
"tiny-blocks"
Expand Down Expand Up @@ -45,9 +46,9 @@
"tiny-blocks/value-object": "^2"
},
"require-dev": {
"infection/infection": "^0.29",
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^11",
"infection/infection": "^0.29",
"squizlabs/php_codesniffer": "^3.10"
},
"scripts": {
Expand Down
76 changes: 42 additions & 34 deletions src/Collectible.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,48 +14,46 @@
/**
* Represents a collection that can be manipulated, iterated, and counted.
*
* @template Key of array-key
* @template Value
* @extends Countable
* @extends IteratorAggregate<Key, Value>
* @template Element
* @extends IteratorAggregate<int, Element>
*/
interface Collectible extends Countable, IteratorAggregate
{
/**
* Creates a new Collectible instance from the given elements.
*
* @param iterable $elements The elements to initialize the collection with.
* @return Collectible<Key, Value> A new Collectible instance.
* @param iterable<Element> $elements The elements to initialize the collection with.
* @return Collectible<Element> A new Collectible instance.
*/
public static function createFrom(iterable $elements): Collectible;
public static function createFrom(iterable $elements): static;

/**
* Creates an empty Collectible instance.
*
* @return Collectible<Key, Value> An empty Collectible instance.
* @return Collectible<Element> An empty Collectible instance.
*/
public static function createFromEmpty(): Collectible;
public static function createFromEmpty(): static;

/**
* Adds one or more elements to the collection.
*
* @param mixed ...$elements The elements to add to the collection.
* @return Collectible<Key, Value> The updated collection.
* @param Element ...$elements The elements to add to the collection.
* @return Collectible<Element> The updated collection.
*/
public function add(mixed ...$elements): Collectible;

/**
* Executes actions on each element in the collection without modifying it.
*
* @param Closure ...$actions The actions to perform on each element.
* @return Collectible<Key, Value> The original collection for chaining.
* @param Closure(Element): void ...$actions The actions to perform on each element.
* @return Collectible<Element> The original collection for chaining.
*/
public function each(Closure ...$actions): Collectible;

/**
* Compares the collection with another collection for equality.
*
* @param Collectible<Key, Value> $other The collection to compare with.
* @param Collectible<Element> $other The collection to compare with.
* @return bool True if the collections are equal, false otherwise.
*/
public function equals(Collectible $other): bool;
Expand All @@ -64,24 +62,24 @@ public function equals(Collectible $other): bool;
* Filters elements in the collection based on the provided predicates.
* If no predicates are provided, all empty or falsy values (e.g., null, false, empty arrays) will be removed.
*
* @param Closure|null ...$predicates
* @return Collectible<Key, Value> The updated collection.
* @param Closure(Element): bool|null ...$predicates
* @return Collectible<Element> The updated collection.
*/
public function filter(?Closure ...$predicates): Collectible;

/**
* Finds the first element matching the provided predicates.
*
* @param Closure ...$predicates The predicates to match.
* @return mixed The first matching element, or null if none found.
* @param Closure(Element): bool ...$predicates The predicates to match.
* @return Element|null The first matching element, or null if none found.
*/
public function findBy(Closure ...$predicates): mixed;

/**
* Retrieves the first element in the collection, or a default value if not found.
*
* @param mixed $defaultValueIfNotFound The default value to return if no element is found.
* @return mixed The first element or the default value.
* @param Element|null $defaultValueIfNotFound The default value to return if no element is found.
* @return Element|null The first element or the default value.
*/
public function first(mixed $defaultValueIfNotFound = null): mixed;

Expand All @@ -96,15 +94,15 @@ public function count(): int;
* Retrieves an element by its index, or a default value if not found.
*
* @param int $index The index of the element to retrieve.
* @param mixed $defaultValueIfNotFound The default value to return if no element is found.
* @return mixed The element at the specified index or the default value.
* @param Element|null $defaultValueIfNotFound The default value to return if no element is found.
* @return Element|null The element at the specified index or the default value.
*/
public function getBy(int $index, mixed $defaultValueIfNotFound = null): mixed;

/**
* Returns an iterator for traversing the collection.
*
* @return Traversable<Key, Value> An iterator for the collection.
* @return Traversable<int, Element> An iterator for the collection.
*/
public function getIterator(): Traversable;

Expand All @@ -118,51 +116,61 @@ public function isEmpty(): bool;
/**
* Retrieves the last element in the collection, or a default value if not found.
*
* @param mixed $defaultValueIfNotFound The default value to return if no element is found.
* @return mixed The last element or the default value.
* @param Element|null $defaultValueIfNotFound The default value to return if no element is found.
* @return Element|null The last element or the default value.
*/
public function last(mixed $defaultValueIfNotFound = null): mixed;

/**
* Applies transformations to each element in the collection and returns a new collection with the transformed
* elements.
*
* @param Closure(Value): Value ...$transformations The transformations to apply.
* @return Collectible<Key, Value> A new collection with the applied transformations.
* @param Closure(Element): Element ...$transformations The transformations to apply.
* @return Collectible<Element> A new collection with the applied transformations.
*/
public function map(Closure ...$transformations): Collectible;

/**
* Removes a specific element from the collection.
*
* @param mixed $element The element to remove.
* @return Collectible<Key, Value> The updated collection.
* @param Element $element The element to remove.
* @return Collectible<Element> The updated collection.
*/
public function remove(mixed $element): Collectible;

/**
* Removes elements from the collection based on the provided filter.
* If no filter is passed, all elements in the collection will be removed.
*
* @param Closure|null $filter The filter to determine which elements to remove.
* @return Collectible<Key, Value> The updated collection.
* @param Closure(Element): bool|null $filter The filter to determine which elements to remove.
* @return Collectible<Element> The updated collection.
*/
public function removeAll(?Closure $filter = null): Collectible;

/**
* Reduces the elements in the collection to a single value by applying an aggregator function.
*
* @param Closure(mixed, Element): mixed $aggregator The function that aggregates the elements.
* It receives the current accumulated value and the current element.
* @param mixed $initial The initial value to start the aggregation.
* @return mixed The final aggregated result.
*/
public function reduce(Closure $aggregator, mixed $initial): mixed;

/**
* Sorts the collection based on the provided order and predicate.
*
* @param Order $order The order in which to sort the collection.
* @param Closure|null $predicate The predicate to use for sorting.
* @return Collectible<Key, Value> The updated collection.
* @param Closure(Element, Element): int|null $predicate The predicate to use for sorting.
* @return Collectible<Element> The updated collection.
*/
public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate = null): Collectible;

/**
* Converts the collection to an array.
*
* @param PreserveKeys $preserveKeys The option to preserve array keys.
* @return array<Key, Value> The resulting array.
* @return array<int, Element> The resulting array.
*/
public function toArray(PreserveKeys $preserveKeys = PreserveKeys::PRESERVE): array;

Expand Down
19 changes: 12 additions & 7 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Closure;
use TinyBlocks\Collection\Internal\Iterators\InternalIterator;
use TinyBlocks\Collection\Internal\Operations\Aggregate\Reduce;
use TinyBlocks\Collection\Internal\Operations\ApplicableOperation;
use TinyBlocks\Collection\Internal\Operations\Compare\Equals;
use TinyBlocks\Collection\Internal\Operations\Filter\Filter;
Expand All @@ -31,9 +32,8 @@
* filtering, mapping, and transforming elements. Internally uses iterators to apply operations
* lazily and efficiently.
*
* @template Key of array-key
* @template Value
* @implements Collectible<Key, Value>
* @template Element
* @implements Collectible<Element>
*/
class Collection implements Collectible
{
Expand All @@ -44,14 +44,14 @@ private function __construct(ApplicableOperation $operation, iterable $elements
$this->iterator = new InternalIterator(elements: $elements, operation: $operation);
}

public static function createFrom(iterable $elements): Collectible
public static function createFrom(iterable $elements): static
{
return new Collection(operation: Create::fromEmpty(), elements: $elements);
return new static(operation: Create::fromEmpty(), elements: $elements);
}

public static function createFromEmpty(): Collectible
public static function createFromEmpty(): static
{
return new Collection(operation: Create::fromEmpty());
return new static(operation: Create::fromEmpty());
}

public function add(mixed ...$elements): Collectible
Expand Down Expand Up @@ -139,6 +139,11 @@ public function removeAll(?Closure $filter = null): Collectible
return $this;
}

public function reduce(Closure $aggregator, mixed $initial): mixed
{
return Reduce::from(elements: $this->iterator)->execute(aggregator: $aggregator, initial: $initial);
}

public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate = null): Collectible
{
$operation = Sort::from(order: $order, predicate: $predicate);
Expand Down
31 changes: 31 additions & 0 deletions src/Internal/Operations/Aggregate/Reduce.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Collection\Internal\Operations\Aggregate;

use Closure;
use TinyBlocks\Collection\Internal\Operations\NonApplicableOperation;

final readonly class Reduce implements NonApplicableOperation
{
public function __construct(private iterable $elements)
{
}

public static function from(iterable $elements): Reduce
{
return new Reduce(elements: $elements);
}

public function execute(Closure $aggregator, mixed $initial): mixed
{
$carry = $initial;

foreach ($this->elements as $element) {
$carry = $aggregator($carry, $element);
}

return $carry;
}
}
14 changes: 8 additions & 6 deletions src/Internal/Operations/Filter/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ public static function from(Closure ...$predicates): Filter
public function apply(iterable $elements): Generator
{
$predicate = $this->predicates
? fn(mixed $value, mixed $key): bool => array_reduce(
$this->predicates,
fn(bool $isValid, Closure $predicate): bool => $isValid && $predicate($value, $key),
true
)
: fn(mixed $value): bool => (bool)$value;
? function (mixed $value, mixed $key): bool {
return array_reduce(
$this->predicates,
static fn(bool $isValid, Closure $predicate): bool => $isValid && $predicate($value, $key),
true
);
}
: static fn(mixed $value): bool => (bool)$value;

foreach ($elements as $key => $value) {
if ($predicate($value, $key)) {
Expand Down
6 changes: 3 additions & 3 deletions src/Internal/Operations/Transform/Each.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ public static function from(Closure ...$actions): Each

public function execute(iterable $elements): void
{
$runActions = function () use ($elements): void {
$runActions = static function ($actions) use ($elements): void {
foreach ($elements as $key => $value) {
foreach ($this->actions as $action) {
foreach ($actions as $action) {
$action($value, $key);
}
}
};

$runActions();
$runActions($this->actions);
}
}
Loading
Loading