Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
ConsecutiveGroupIterator
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
7 / 7
12
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
 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
2
 next
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rewind
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 loadChunk
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
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 dynamically based on a user-defined condition.
25 *
26 * This iterator chunks elements from a traversable source, creating groups where
27 * each new element is added to the current chunk until the provided callback
28 * returns `false`, signaling the start of a new chunk.
29 *
30 * ## Usage Example:
31 *
32 * @example Grouping consecutive equal elements
33 * ```php
34 * use FastForward\Iterator\ConsecutiveGroupIterator;
35 * use ArrayIterator;
36 *
37 * $data = new ArrayIterator([1, 1, 2, 2, 2, 3, 4, 4, 5]);
38 *
39 * $chunkedIterator = new ConsecutiveGroupIterator($data, fn($prev, $curr) => $prev === $curr);
40 *
41 * foreach ($chunkedIterator as $chunk) {
42 *     print_r($chunk);
43 * }
44 * // Outputs:
45 * // [1, 1]
46 * // [2, 2, 2]
47 * // [3]
48 * // [4, 4]
49 * // [5]
50 * ```
51 *
52 * **Note:** The chunking behavior is defined by the callback function.
53 *
54 * @since 1.0.0
55 */
56class ConsecutiveGroupIterator extends CountableIteratorIterator
57{
58    /**
59     * @var array<int, mixed> the buffer storing the current chunk of elements
60     */
61    private array $buffer = [];
62
63    /**
64     * @var int the current group key for the iterator
65     */
66    private int $groupKey = 0;
67
68    /**
69     * Initializes the ConsecutiveGroupIterator.
70     *
71     * @param iterable $iterator the iterator containing values to be chunked
72     * @param Closure $callback The function that determines whether elements should be in the same chunk.
73     *                          It receives two arguments: `$previous` and `$current`,
74     *                          and must return `true` to keep them together or `false` to start a new chunk.
75     */
76    public function __construct(
77        iterable $iterator,
78        private  Closure $callback
79    ) {
80        parent::__construct(new IterableIterator($iterator));
81    }
82
83    /**
84     * Retrieves the current chunk of elements.
85     *
86     * @return array<int, mixed> the current chunk as an array
87     */
88    public function current(): array
89    {
90        return $this->buffer;
91    }
92
93    /**
94     * Retrieves the current group key.
95     *
96     * @return int|null the current group key, or null if the iterator is not valid
97     */
98    public function key(): ?int
99    {
100        return $this->valid() ? $this->groupKey : null;
101    }
102
103    /**
104     * Advances to the next chunk of elements.
105     *
106     * @return void
107     */
108    public function next(): void
109    {
110        if ($this->loadChunk()) {
111            ++$this->groupKey;
112        }
113    }
114
115    /**
116     * Checks if the current chunk contains valid elements.
117     *
118     * @return bool true if a chunk exists, false otherwise
119     */
120    public function valid(): bool
121    {
122        return [] !== $this->buffer;
123    }
124
125    /**
126     * Resets the iterator and prepares the first chunk.
127     *
128     * @return void
129     */
130    public function rewind(): void
131    {
132        parent::rewind();
133        $this->loadChunk();
134        $this->groupKey = 0;
135    }
136
137    /**
138     * Loads the next chunk of elements based on the callback condition.
139     *
140     * This method fills the buffer with consecutive elements that satisfy the callback condition.
141     *
142     * @return bool true if a chunk was loaded, false otherwise
143     */
144    private function loadChunk(): bool
145    {
146        $this->buffer = [];
147
148        while (parent::valid()) {
149            $currentValue = parent::current();
150
151            if ([] !== $this->buffer) {
152                $lastValue = $this->buffer[array_key_last($this->buffer)];
153
154                if (! ($this->callback)($lastValue, $currentValue)) {
155                    break;
156                }
157            }
158
159            $this->buffer[] = $currentValue;
160            parent::next();
161        }
162
163        return [] !== $this->buffer;
164    }
165}