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