OrderedCollection   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 71
c 1
b 0
f 0
dl 0
loc 260
ccs 88
cts 88
cp 1
rs 9.36
wmc 38

20 Methods

Rating   Name   Duplication   Size   Complexity  
A contains() 0 3 1
A clear() 0 4 1
A toArray() 0 5 1
B search() 0 29 11
A remove() 0 11 2
A addAll() 0 9 2
A stream() 0 3 1
A at() 0 9 2
A offsetExists() 0 5 1
A empty() 0 3 1
A forEach() 0 6 2
A add() 0 6 1
A __construct() 0 3 1
A replace() 0 6 2
A count() 0 3 1
A getIterator() 0 3 1
A offsetUnset() 0 7 1
A offsetGet() 0 6 1
A offsetSet() 0 7 2
A sortElements() 0 13 3
1
<?php
2
3
namespace Bdf\Collection;
4
5
use ArrayIterator;
6
use BadMethodCallException;
7
use Bdf\Collection\Stream\ArrayStream;
8
use Bdf\Collection\Stream\StreamInterface;
9
use OutOfBoundsException;
10
use function array_merge;
11
use function array_search;
12
use function array_slice;
13
use function count;
14
use function intdiv;
15
use function is_array;
16
use function iterator_to_array;
17
use function sort;
18
use function usort;
19
20
/**
21
 * Collection implementation that provides an ordering on its elements
22
 *
23
 * This collection is lazy ordered : it sorts elements only when necessary
24
 *
25
 * Complexity :
26
 * - add()      : O(1)      But needs to be sorted after
27
 * - contains() : O(log(n)) Sort elements if not sorted
28
 * - remove()   : O(n)      The array is copied, but keep the order
29
 * - forEach()  : O(n)      Sort elements if not sorted
30
 * - at()       : O(1)      Sort elements if not sorted
31
 * - search()   : O(log(n)) Sort elements if not sorted
32
 *
33
 * @template T
34
 * @implements OrderedCollectionInterface<T>
35
 */
36
class OrderedCollection implements OrderedCollectionInterface
37
{
38
    /**
39
     * @var callable|null
40
     */
41
    private $comparator;
42
43
    /**
44
     * @var array
45
     */
46
    private $elements = [];
47
48
    /**
49
     * @var bool
50
     */
51
    private $sorted = true;
52
53
54
    /**
55
     * SortedCollection constructor.
56
     *
57
     * @param callable|null $comparator The elements comparator. If null use natural order
58
     *     The comparator must takes the two elements (A, B) to compare as parameters
59
     *     And must return an integer as : <= -1 for A < B, = 0 for A = B and >= 1 for A > B
60
     */
61 29
    public function __construct(callable $comparator = null)
62
    {
63 29
        $this->comparator = $comparator;
64 29
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69 9
    public function add($element): bool
70
    {
71 9
        $this->elements[] = $element;
72 9
        $this->sorted = count($this->elements) === 1;
73
74 9
        return true;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 6
    public function addAll(iterable $elements): bool
81
    {
82 6
        foreach ($elements as $element) {
83 5
            $this->elements[] = $element;
84
        }
85
86 6
        $this->sorted = count($this->elements) <= 1;
87
88 6
        return true;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 19
    public function replace(iterable $elements): bool
95
    {
96 19
        $this->elements = is_array($elements) ? array_values($elements) : iterator_to_array($elements, false);
97 19
        $this->sorted = count($this->elements) <= 1;
98
99 19
        return true;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 2
    public function contains($element, bool $strict = false): bool
106
    {
107 2
        return $this->search($element, $strict) !== false;
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 1
    public function remove($element, bool $strict = false): bool
114
    {
115 1
        $key = $this->search($element, $strict);
116
117 1
        if ($key === false) {
118 1
            return false;
119
        }
120
121 1
        $this->offsetUnset($key);
122
123 1
        return true;
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129 1
    public function clear(): void
130
    {
131 1
        $this->elements = [];
132 1
        $this->sorted = true;
133 1
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138 1
    public function empty(): bool
139
    {
140 1
        return empty($this->elements);
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 1
    public function forEach(callable $consumer): void
147
    {
148 1
        $this->sortElements();
149
150 1
        foreach ($this->elements as $position => $element) {
151 1
            $consumer($element, $position);
152
        }
153 1
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 18
    public function toArray(): array
159
    {
160 18
        $this->sortElements();
161
162 18
        return $this->elements;
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168 10
    public function getIterator(): \Traversable
169
    {
170 10
        return new ArrayIterator($this->toArray());
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176 2
    public function count(): int
177
    {
178 2
        return count($this->elements);
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 1
    public function stream(): StreamInterface
185
    {
186 1
        return new ArrayStream($this->toArray());
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 6
    public function search($element, bool $strict = false)
193
    {
194 6
        $this->sortElements();
195
196 6
        $first = 0;
197 6
        $last  = count($this->elements) - 1;
198
199
        // Until 3000 elements, native array search is faster
200 6
        if ($last < 3000) {
201 4
            return array_search($element, $this->elements, $strict);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_search($ele...his->elements, $strict) also could return the type string which is incompatible with the return type mandated by Bdf\Collection\OrderedCo...tionInterface::search() of false|integer.
Loading history...
202
        }
203
204
        // Perform binary search
205 2
        while ($first <= $last) {
206 2
            $key = intdiv($first + $last, 2);
207 2
            $current = $this->elements[$key];
208
209 2
            if ((!$strict && $element == $current) || ($strict && $element === $current)) {
210 2
                return $key;
211
            }
212
213 2
            if (($this->comparator && ($this->comparator)($element, $current) < 0) || (!$this->comparator && $element < $current)) {
214 2
                $last = $key - 1;
215
            } else {
216 2
                $first = $key + 1;
217
            }
218
        }
219
220 2
        return false;
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226 2
    public function at(int $position)
227
    {
228 2
        $this->sortElements();
229
230 2
        if (!isset($this->elements[$position])) {
231 1
            throw new OutOfBoundsException('Invalid position '.$position);
232
        }
233
234 1
        return $this->elements[$position];
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     */
240 2
    public function offsetExists($offset): bool
241
    {
242 2
        $this->sortElements();
243
244 2
        return isset($this->elements[$offset]);
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250
    #[\ReturnTypeWillChange]
251 1
    public function offsetGet($offset)
252
    {
253 1
        $this->sortElements();
254
255 1
        return $this->elements[$offset];
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 3
    public function offsetUnset($offset): void
262
    {
263 3
        $this->sortElements();
264
265 3
        $this->elements = array_merge(
266 3
            array_slice($this->elements, 0, $offset),
267 3
            array_slice($this->elements, $offset + 1)
268
        );
269 3
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274 2
    public function offsetSet($offset, $value): void
275
    {
276 2
        if ($offset !== null) {
277 1
            throw new BadMethodCallException('Cannot set a value into an OrderedCollection');
278
        }
279
280 1
        $this->add($value);
281 1
    }
282
283 27
    private function sortElements(): void
284
    {
285 27
        if ($this->sorted) {
286 16
            return;
287
        }
288
289 19
        if ($this->comparator) {
290 2
            usort($this->elements, $this->comparator);
291
        } else {
292 17
            sort($this->elements);
293
        }
294
295 19
        $this->sorted = true;
296 19
    }
297
}
298