Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
UniqueIteratorIterator
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
5 / 5
10
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
 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%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 rewind
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 skipDuplicates
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
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
21/**
22 * Filters duplicate values from an iterator, ensuring uniqueness.
23 *
24 * This iterator allows traversing an iterable while maintaining a record of seen values,
25 * returning only the first occurrence of each unique value. Subsequent occurrences are skipped.
26 *
27 * ## Usage Example:
28 *
29 * @example Removing duplicate values
30 * ```php
31 * use FastForward\Iterator\UniqueIteratorIterator;
32 * use ArrayIterator;
33 *
34 * $data = new ArrayIterator([1, 2, 2, 3, 4, 4, 5]);
35 * $uniqueIterator = new UniqueIteratorIterator($data);
36 *
37 * foreach ($uniqueIterator as $num) {
38 *     echo $num . ' '; // Outputs: 1 2 3 4 5
39 * }
40 * ```
41 * @example Case-insensitive uniqueness
42 * ```php
43 * use FastForward\Iterator\UniqueIteratorIterator;
44 * use ArrayIterator;
45 *
46 * $data = new ArrayIterator(['a', 'A', 'b', 'B', 'a']);
47 * $uniqueIterator = new UniqueIteratorIterator($data, false);
48 *
49 * foreach ($uniqueIterator as $char) {
50 *     echo $char . ' '; // Outputs: a A b B a (case-sensitive comparison disabled)
51 * }
52 * ```
53 *
54 * **Note:** This iterator preserves the order of first occurrences.
55 *
56 * @since 1.0.0
57 */
58class UniqueIteratorIterator extends CountableIteratorIterator
59{
60    /**
61     * @var array<int|string, mixed> stores seen values to ensure uniqueness
62     */
63    private array $seen = [];
64
65    /**
66     * @var int the current position for the unique elements
67     */
68    private int $position = 0;
69
70    /**
71     * Initializes the UniqueIteratorIterator.
72     *
73     * @param iterable $iterator the iterator to filter for unique values
74     * @param bool $strict whether to use strict comparison (default: true)
75     * @param bool $caseSensitive whether to use case-sensitive comparison (default: true)
76     */
77    public function __construct(
78        iterable $iterator,
79        private  bool $strict = true,
80        private  bool $caseSensitive = true,
81    ) {
82        parent::__construct(new IterableIterator($iterator));
83    }
84
85    /**
86     * Retrieves the normalized sequential key for the current unique element.
87     *
88     * @return int the zero-based position of the current unique value
89     */
90    public function key(): int
91    {
92        return $this->position;
93    }
94
95    /**
96     * Advances to the next unique element.
97     *
98     * @return void
99     */
100    public function next(): void
101    {
102        parent::next();
103        $this->skipDuplicates();
104
105        if (parent::valid()) {
106            ++$this->position;
107        }
108    }
109
110    /**
111     * Resets the iterator and clears the seen values.
112     *
113     * @return void
114     */
115    public function rewind(): void
116    {
117        parent::rewind();
118        $this->seen = [];
119        $this->position = 0;
120        $this->skipDuplicates();
121    }
122
123    /**
124     * Skips values that have already been encountered and stores the current unique value.
125     *
126     * @return void
127     */
128    private function skipDuplicates(): void
129    {
130        while (parent::valid()) {
131            $value = parent::current();
132
133            if (! $this->caseSensitive && \is_string($value)) {
134                $value = mb_strtolower($value);
135            }
136
137            if (! \in_array($value, $this->seen, $this->strict)) {
138                $this->seen[] = $value;
139
140                return;
141            }
142
143            parent::next();
144        }
145    }
146}