Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.97% covered (success)
96.97%
32 / 33
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
InterleaveIteratorIterator
96.97% covered (success)
96.97%
32 / 33
83.33% covered (warning)
83.33%
5 / 6
17
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 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%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 next
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 valid
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 rewind
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
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 Iterator;
22use InvalidArgumentException;
23
24/**
25 * Interleaves elements from multiple iterators in a round-robin fashion.
26 *
27 * This iterator alternates between multiple traversable sources,
28 * returning one element from each before cycling back to the first.
29 * The iteration stops once all iterators are exhausted.
30 *
31 * ## Usage Example:
32 *
33 * @example Interleaving two iterators
34 * ```php
35 * use FastForward\Iterator\InterleaveIteratorIterator;
36 * use ArrayIterator;
37 *
38 * $dataSetOne = new ArrayIterator([1, 3, 5]);
39 * $dataSetTwo = new ArrayIterator([2, 4, 6]);
40 *
41 * $interleavedIterator = new InterleaveIteratorIterator($dataSetOne, $dataSetTwo);
42 *
43 * foreach ($interleavedIterator as $value) {
44 *     echo $value . ' '; // Outputs: 1 2 3 4 5 6
45 * }
46 * ```
47 *
48 * **Note:** The iterator stops once all sources are exhausted.
49 *
50 * @since 1.0.0
51 */
52class InterleaveIteratorIterator extends CountableIterator
53{
54    /**
55     * @var array<int, Iterator> list of active iterators
56     */
57    private array $iterators;
58
59    /**
60     * @var int the current iterator index
61     */
62    private int $currentIndex = 0;
63
64    /**
65     * @var int the normalized sequential numeric key for yielded values without string keys
66     */
67    private int $position = 0;
68
69    /**
70     * Initializes the InterleaveIteratorIterator.
71     *
72     * @param iterable ...$iterators The iterators to be interleaved.
73     *
74     * @throws InvalidArgumentException if no iterators are provided
75     */
76    public function __construct(iterable ...$iterators)
77    {
78        if ([] === $iterators) {
79            throw new InvalidArgumentException('At least one iterator must be provided.');
80        }
81
82        $this->iterators = array_map(
83            static fn(iterable $iterator): Iterator => new IterableIterator($iterator),
84            $iterators
85        );
86
87        // Inicializa os iteradores
88        foreach ($this->iterators as $iterator) {
89            $iterator->rewind();
90        }
91    }
92
93    /**
94     * Retrieves the current element from the active iterator.
95     *
96     * @return mixed the current element
97     */
98    public function current(): mixed
99    {
100        return $this->iterators[$this->currentIndex]->current();
101    }
102
103    /**
104     * Retrieves the current key from the active iterator or a normalized sequential key.
105     *
106     * If the active iterator's current key is a string, it is returned directly.
107     * Otherwise, a normalized sequential numeric key is returned based on the position of yielded values without string keys.
108     *
109     * @return string|int the current key
110     */
111    public function key(): string|int
112    {
113        $iteratorKey = $this->iterators[$this->currentIndex]->key();
114
115        if (\is_string($iteratorKey)) {
116            return $iteratorKey;
117        }
118
119        return $this->position;
120    }
121
122    /**
123     * @return void
124     */
125    public function next(): void
126    {
127        $currentKey = $this->iterators[$this->currentIndex]->key();
128
129        $this->iterators[$this->currentIndex]->next();
130
131        if (! \is_string($currentKey)) {
132            ++$this->position;
133        }
134
135        if (! $this->valid()) {
136            return;
137        }
138
139        do {
140            $this->currentIndex = ($this->currentIndex + 1) % \count($this->iterators);
141        } while (! $this->iterators[$this->currentIndex]->valid() && $this->valid());
142    }
143
144    /**
145     * Checks if at least one iterator still has elements.
146     *
147     * @return bool true if there are remaining elements, false otherwise
148     */
149    public function valid(): bool
150    {
151        foreach ($this->iterators as $iterator) {
152            if ($iterator->valid()) {
153                return true;
154            }
155        }
156
157        return false;
158    }
159
160    /**
161     * @return void
162     */
163    public function rewind(): void
164    {
165        $this->currentIndex = 0;
166        $this->position = 0;
167
168        foreach ($this->iterators as $iterator) {
169            $iterator->rewind();
170        }
171
172        if (! $this->valid()) {
173            return;
174        }
175
176        while (! $this->iterators[$this->currentIndex]->valid()) {
177            $this->currentIndex = ($this->currentIndex + 1) % \count($this->iterators);
178        }
179    }
180}