Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
GroupByIteratorIterator
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
6 / 6
7
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 current
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 next
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of php-fast-forward/iterators.
7 *
8 * This source file is subject to the license that is bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2025-2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/iterators
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\Iterator;
20
21use Closure;
22
23/**
24 * Groups elements from an iterator based on a callback function.
25 *
26 * This iterator aggregates elements into associative arrays where the keys are
27 * determined by a user-defined function (`$groupBy`). Each key contains an array
28 * of elements that share the same computed group.
29 *
30 * ## Usage Example:
31 *
32 * @example Grouping by age
33 * ```php
34 * use FastForward\Iterator\GroupByIteratorIterator;
35 * use ArrayIterator;
36 *
37 * $data = new ArrayIterator([
38 *     ['name' => 'Alice', 'age' => 25],
39 *     ['name' => 'Bob', 'age' => 30],
40 *     ['name' => 'Charlie', 'age' => 25],
41 * ]);
42 *
43 * $grouped = new GroupByIteratorIterator($data, fn($item) => $item['age']);
44 *
45 * foreach ($grouped as $age => $group) {
46 *     echo "Age: $age\n";
47 *     print_r($group);
48 * }
49 * // Outputs:
50 * // Age: 25
51 * // [['name' => 'Alice', 'age' => 25], ['name' => 'Charlie', 'age' => 25]]
52 * // Age: 30
53 * // [['name' => 'Bob', 'age' => 30]]
54 * ```
55 *
56 * **Note:** The iterator must be rewound before iterating again to ensure correct grouping.
57 *
58 * @since 1.0.0
59 */
60class GroupByIteratorIterator extends CountableIteratorIterator
61{
62    /**
63     * @var array<string|int, array<int, mixed>> holds grouped elements
64     */
65    private array $groups = [];
66
67    /**
68     * Initializes the GroupByIteratorIterator.
69     *
70     * @param iterable $iterator the iterator containing values to be grouped
71     * @param Closure $groupBy a function that determines the group key for each element
72     */
73    public function __construct(
74        iterable $iterator,
75        private  Closure $groupBy
76    ) {
77        parent::__construct(new IterableIterator($iterator));
78    }
79
80    /**
81     * Rewinds the iterator and reprocesses the grouping.
82     *
83     * This ensures that the grouping is correctly recomputed when the iterator is reset.
84     *
85     * @return void
86     */
87    public function rewind(): void
88    {
89        parent::rewind();
90        $this->groups = [];
91
92        foreach ($this->getInnerIterator() as $key => $value) {
93            $groupKey                  = ($this->groupBy)($value, $key);
94            $this->groups[$groupKey][] = $value;
95        }
96
97        reset($this->groups);
98    }
99
100    /**
101     * Retrieves the current group of elements.
102     *
103     * @return array<int, mixed> the current group of elements
104     */
105    public function current(): array
106    {
107        return current($this->groups);
108    }
109
110    /**
111     * Retrieves the key of the current group.
112     *
113     * @return mixed the computed key representing the current group
114     */
115    public function key(): mixed
116    {
117        return key($this->groups);
118    }
119
120    /**
121     * Advances to the next group in the iterator.
122     *
123     * @return void
124     */
125    public function next(): void
126    {
127        next($this->groups);
128    }
129
130    /**
131     * Checks if the current position is valid.
132     *
133     * @return bool true if a valid group exists, false otherwise
134     */
135    public function valid(): bool
136    {
137        return null !== key($this->groups);
138    }
139}