Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
20 / 20
CRAP
100.00% covered (success)
100.00%
1 / 1
Collection
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
20 / 20
32
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 initialize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setComparator
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getIndex
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isEmpty
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getElement
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 validateExistingKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 findElementKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getElements
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findElementKeys
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 hasKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 add
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 validateNewKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 validateValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 update
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 delete
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFirst
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLast
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getNth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * @author: Doug Wilbourne (dougwilbourne@gmail.com)
5 */
6
7declare(strict_types=1);
8
9namespace pvc\struct\collection;
10
11use ArrayIterator;
12use IteratorIterator;
13use pvc\interfaces\struct\collection\CollectionInterface;
14use pvc\interfaces\struct\types\value\ValTesterInterface;
15use pvc\struct\collection\err\DuplicateKeyException;
16use pvc\struct\types\err\InvalidKeyException;
17use pvc\struct\types\err\InvalidValueException;
18use pvc\struct\types\err\NonExistentKeyException;
19use pvc\struct\types\id\IdTypeTrait;
20use pvc\struct\types\value\ValTesterTrait;
21
22/**
23 * Class Collection
24 * @template KeyType of array-key
25 * @template ElementType
26 *
27 * @extends IteratorIterator<mixed, ElementType, ArrayIterator<KeyType, ElementType>>
28 * @implements CollectionInterface<KeyType, ElementType>
29 *
30 * elements in a collection cannot be null
31 */
32class Collection extends IteratorIterator implements CollectionInterface
33{
34    /**
35     * @var ArrayIterator<KeyType, ElementType>
36     * inner iterator
37     */
38    protected ArrayIterator $iterator;
39
40    /**
41     * @var ?callable(ElementType, ElementType): int $comparator ;
42     */
43    protected $comparator;
44
45    /**
46     * if you do not want to rely on static analysis to ensure validation of
47     * keys and values
48     */
49    use IdTypeTrait;
50    use ValTesterTrait;
51
52    /**
53     * @param ?ArrayIterator<KeyType, ElementType>  $iterator
54     */
55    public function __construct(?ArrayIterator $iterator = null)
56    {
57        if (is_null($iterator)) {
58            $iterator = new ArrayIterator();
59        }
60        $this->iterator = $iterator;
61        parent::__construct($iterator);
62    }
63
64    /**
65     * initialize
66     *
67     * @return void
68     */
69    public function initialize(): void
70    {
71        $this->iterator = new ArrayIterator();
72    }
73
74    /**
75     * @param ?callable(ElementType, ElementType): int  $comparator
76     *
77     * @return void
78     */
79    public function setComparator($comparator): void
80    {
81        $this->comparator = $comparator;
82        if ($this->comparator !== null) {
83            $this->iterator->uasort($this->comparator);
84        }
85    }
86
87    /**
88     * @param  KeyType  $key
89     *
90     * @return non-negative-int
91     */
92    public function getIndex($key): int
93    {
94        $this->validateExistingKey($key);
95        $result = $i = 0;
96        foreach ($this->iterator as $n => $element) {
97            if ($n === $key) {
98                $result = $i;
99            }
100            $i++;
101        }
102        return $result;
103    }
104
105    /**
106     * isEmpty returns whether the collection is empty or not
107     *
108     * @return bool
109     */
110    public function isEmpty(): bool
111    {
112        return (0 == $this->count());
113    }
114
115    /**
116     * count
117     *
118     * @return non-negative-int
119     */
120    public function count(): int
121    {
122        return $this->iterator->count();
123    }
124
125    /**
126     * getElement
127     *
128     * @param  KeyType  $key
129     *
130     * @return ElementType
131     */
132    public function getElement($key): mixed
133    {
134        $this->validateExistingKey($key);
135
136        /**
137         * element cannot be null
138         */
139        $element = $this->iterator->offsetGet($key);
140        assert(!is_null($element));
141        return $element;
142    }
143
144
145    /**
146     * validateExistingKey ensures that the key is both valid and exists in the collection
147     *
148     * @param  KeyType  $key
149     *
150     * @throws InvalidKeyException
151     * @throws NonExistentKeyException
152     */
153    protected function validateExistingKey($key): void
154    {
155        if (false === $this->testIdType($key)) {
156            throw new InvalidKeyException((string)$key);
157        }
158        if (!$this->iterator->offsetExists($key)) {
159            throw new NonExistentKeyException((string)$key);
160        }
161    }
162
163    /**
164     * @function findElementKey
165     *
166     * @param  ValTesterInterface<ElementType>  $valTester
167     *
168     * @return KeyType|null
169     */
170    public function findElementKey(ValTesterInterface $valTester
171    ): int|string|null {
172        return array_find_key($this->getElements(), [$valTester, 'testValue']);
173    }
174
175    /**
176     * now implement methods explicitly defined in the interface
177     */
178
179    /**
180     * @function getElements
181     * @return array<KeyType, ElementType>
182     */
183    public function getElements(): array
184    {
185        /**
186         * keys are preserved by default
187         */
188        return iterator_to_array($this->iterator);
189    }
190
191    /**
192     *
193     * @param  ValTesterInterface<ElementType>  $valTester
194     *
195     * @return array<KeyType>
196     */
197    public function findElementKeys(ValTesterInterface $valTester): array
198    {
199        $elements = array_filter($this->getElements(), [$valTester, 'testValue']
200        );
201        return array_keys($elements);
202    }
203
204    /**
205     * hasKey
206     * @param KeyType $key
207     *
208     * @return bool
209     */
210    public function hasKey($key): bool
211    {
212        if (false === $this->testIdType($key)) {
213            throw new InvalidKeyException((string)$key);
214        }
215        return $this->iterator->offsetExists($key);
216    }
217
218    /**
219     * add
220     *
221     * Unlike when you are dealing with a raw array, using an existing key will throw an exception instead
222     * of overwriting an existing entry in the array.  Use update to be explicit about updating an entry.
223     *
224     * @param  KeyType  $key
225     * @param  ElementType  $element
226     *
227     * @throws DuplicateKeyException|InvalidKeyException|InvalidValueException
228     */
229    public function add($key, $element): void
230    {
231        $this->validateNewKey($key);
232        $this->validateValue($element);
233
234        $this->iterator->offsetSet($key, $element);
235
236        if ($this->comparator !== null) {
237            $this->iterator->uasort($this->comparator);
238        }
239    }
240
241    /**
242     * validateNewKey ensures that the key does not exist in the collection
243     *
244     * @param  KeyType  $key
245     */
246    protected function validateNewKey($key): void
247    {
248        if (false === $this->testIdType($key)) {
249            throw new InvalidKeyException((string)$key);
250        }
251        if ($this->iterator->offsetExists($key)) {
252            throw new DuplicateKeyException((string) $key);
253        }
254    }
255
256    /**
257     * validateValue
258     *
259     * @param  mixed  $value
260     *
261     * @return void
262     */
263    protected function validateValue($value): void
264    {
265        if (false === $this->testValue($value)) {
266            throw new InvalidValueException();
267        }
268    }
269
270    /**
271     * update assigns a new element to the entry with index $key
272     *
273     * @param  KeyType  $key
274     * @param  ElementType  $element
275     *
276     * @throws InvalidKeyException
277     * @throws NonExistentKeyException
278     */
279    public function update($key, $element): void
280    {
281        $this->validateExistingKey($key);
282        $this->validateValue($element);
283
284        $this->iterator->offsetSet($key, $element);
285
286        if ($this->comparator !== null) {
287            $this->iterator->uasort($this->comparator);
288        }
289    }
290
291    /**
292     * delete removes an element from the collection.  Unlike unset, this operation throws an exception if the
293     * key does not exist.
294     *
295     * @param  KeyType  $key
296     *
297     * @return void
298     * @throws InvalidKeyException
299     * @throws NonExistentKeyException
300     */
301    public function delete($key): void
302    {
303        $this->validateExistingKey($key);
304        $this->iterator->offsetUnset($key);
305    }
306
307    /**
308     * @return ElementType|null
309     */
310    public function getFirst(): mixed
311    {
312        return array_values($this->getElements())[0] ?? null;
313    }
314
315    /**
316     * @return ElementType|null
317     */
318    public function getLast(): mixed
319    {
320        return array_values($this->getElements())[count($this->getElements())
321        - 1] ?? null;
322    }
323
324    /**
325     * @param  non-negative-int  $index
326     *
327     * @return ElementType|null
328     */
329    public function getNth(int $index): mixed
330    {
331        return array_values($this->getElements())[$index] ?? null;
332    }
333
334}