Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
RangeIterator
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
9 / 9
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 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%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 rewind
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 valid
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 count
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 isAtBoundary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 wouldOvershoot
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
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 Countable;
23use InvalidArgumentException;
24
25/**
26 * An iterator that behaves like PHP's `range()` function.
27 * It supports both ascending and descending sequences and allows iteration
28 * over floating-point or integer ranges.
29 *
30 * ## Usage Example:
31 *
32 * @example Iterating Over an Integer Range
33 * ```php
34 * use FastForward\Iterator\RangeIterator;
35 *
36 * $iterator = new RangeIterator(1, 10, 2);
37 *
38 * foreach ($iterator as $key => $value) {
39 *     echo "[$key] => $value\n";
40 * }
41 * // Output:
42 * // [0] => 1
43 * // [1] => 3
44 * // [2] => 5
45 * // [3] => 7
46 * // [4] => 9
47 * ```
48 * @example Iterating Over a Floating-Point Range
49 * ```php
50 * use FastForward\Iterator\RangeIterator;
51 *
52 * $iterator = new RangeIterator(0.5, 2.5, 0.5);
53 *
54 * foreach ($iterator as $value) {
55 *     echo $value . "\n";
56 * }
57 * // Output:
58 * // 0.5
59 * // 1.0
60 * // 1.5
61 * // 2.0
62 * // 2.5
63 * ```
64 * @example Iterating Over a Floating-Point Range Including the Boundary
65 * ```php
66 * use FastForward\Iterator\RangeIterator;
67 *
68 * $iterator = new RangeIterator(0, 5, 1.5, true);
69 *
70 * foreach ($iterator as $value) {
71 *     echo $value . "\n";
72 * }
73 * // Output:
74 * // 0
75 * // 1.5
76 * // 3.0
77 * // 4.5
78 * // 5.0
79 * ```
80 *
81 * **Note:** If the `step` is larger than the absolute difference between `start` and `end`,
82 * an `InvalidArgumentException` is thrown.
83 *
84 * @since 1.0.0
85 */
86class RangeIterator implements Iterator, Countable
87{
88    /**
89     * @var float floating-point comparison tolerance
90     */
91    private const float EPSILON = 1.0E-12;
92
93    /**
94     * @var int the current key (index) in the iteration
95     */
96    private int $key = 0;
97
98    /**
99     * @var float|int the current value in the iteration
100     */
101    private float|int $current;
102
103    /**
104     * @var float|int the step size for each iteration
105     */
106    private  float|int $step;
107
108    /**
109     * @var bool indicates whether the iterator should force the boundary value when the next step would overshoot the range
110     */
111    private bool $boundaryYielded = false;
112
113    /**
114     * Initializes the RangeIterator.
115     *
116     * @param float|int $start the starting value of the range
117     * @param float|int $end the ending value of the range
118     * @param float|int $step the step size between values (must be positive)
119     * @param bool $includeBoundary whether the iterator should force the end value when the next step would overshoot it
120     *
121     * @throws InvalidArgumentException if step is non-positive or greater than the range size
122     */
123    public function __construct(
124        private  float|int $start,
125        private  float|int $end,
126        float|int $step = 1,
127        private  bool $includeBoundary = false
128    ) {
129        if ($step <= 0) {
130            throw new InvalidArgumentException('Step must be a positive integer or float.');
131        }
132
133        $rangeSize = abs($this->end - $this->start);
134
135        if ($rangeSize > 0 && $step > $rangeSize) {
136            throw new InvalidArgumentException(
137                'Step cannot be greater than the absolute difference between start and end.'
138            );
139        }
140
141        $this->step    = ($start > $end) ? -$step : $step;
142        $this->current = $start;
143    }
144
145    /**
146     * Returns the current value in the range.
147     *
148     * @return float|int the current value
149     */
150    public function current(): float|int
151    {
152        return $this->current;
153    }
154
155    /**
156     * Returns the current key (index).
157     *
158     * @return int the current index in the iteration
159     */
160    public function key(): int
161    {
162        return $this->key;
163    }
164
165    /**
166     * Moves to the next value in the range.
167     *
168     * @return void
169     */
170    public function next(): void
171    {
172        $projected = $this->current + $this->step;
173
174        if (
175            $this->includeBoundary
176            && ! $this->boundaryYielded
177            && ! $this->isAtBoundary($this->current)
178            && $this->wouldOvershoot($projected)
179        ) {
180            $this->current = $this->end;
181            $this->boundaryYielded = true;
182            ++$this->key;
183
184            return;
185        }
186
187        $this->current = $projected;
188        ++$this->key;
189    }
190
191    /**
192     * Resets the iterator to the start of the range.
193     *
194     * @return void
195     */
196    public function rewind(): void
197    {
198        $this->boundaryYielded = false;
199        $this->current = $this->start;
200        $this->key = 0;
201    }
202
203    /**
204     * Checks if the current position is within the valid range.
205     *
206     * @return bool true if the current value is within the valid range, false otherwise
207     */
208    public function valid(): bool
209    {
210        return $this->step > 0
211            ? $this->current <= $this->end + self::EPSILON
212            : $this->current >= $this->end - self::EPSILON;
213    }
214
215    /**
216     * Counts the total number of steps in the range.
217     *
218     * @return int the number of elements in the range
219     */
220    public function count(): int
221    {
222        $length = abs($this->end - $this->start);
223
224        if ($length <= self::EPSILON) {
225            return 1;
226        }
227
228        $step = abs($this->step);
229        $quotient = $length / $step;
230        $wholeSteps = (int) floor($quotient + self::EPSILON);
231        $count = $wholeSteps + 1;
232        $hasRemainder = abs($quotient - $wholeSteps) > self::EPSILON;
233
234        if ($this->includeBoundary && $hasRemainder) {
235            ++$count;
236        }
237
238        return $count;
239    }
240
241    /**
242     * Checks whether the given value is effectively equal to the range boundary.
243     *
244     * This method accounts for floating-point precision issues by using a tolerance value (EPSILON)
245     * to determine if the current value is close enough to the end value to be considered at the boundary.
246     *
247     * @param float|int $value the value to check against the boundary
248     *
249     * @return bool true if the value is effectively at the boundary, false otherwise
250     */
251    private function isAtBoundary(float|int $value): bool
252    {
253        return abs($value - $this->end) <= self::EPSILON;
254    }
255
256    /**
257     * Checks whether the projected next value would exceed the range boundary.
258     *
259     * This method is used to determine if the next step would overshoot the end value,
260     * which is important when the `includeBoundary` option is enabled to ensure thatthe boundary value is yielded when appropriate.
261     *
262     * @param float|int $projected the next value after applying the step
263     *
264     * @return bool true if the projected value would overshoot the end boundary, false otherwise
265     */
266    private function wouldOvershoot(float|int $projected): bool
267    {
268        return $this->step > 0
269            ? $projected > $this->end + self::EPSILON
270            : $projected < $this->end - self::EPSILON;
271    }
272}