Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
114 / 114
100.00% covered (success)
100.00%
23 / 23
CRAP
100.00% covered (success)
100.00%
1 / 1
MultiMap
100.00% covered (success)
100.00%
114 / 114
100.00% covered (success)
100.00%
23 / 23
71
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
 add
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
10
 garbageCollect
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 garbageCollectKeys
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 garbageCollectValues
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getSingle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getMultiple
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 set
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 setMultiple
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 setSingle
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 remove
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 removeMultiple
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 removeSingle
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 remap
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 removeKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 removeKeySingle
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getSiblings
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getSiblingsMultiple
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getSiblingsSingle
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 keys
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 values
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
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Stdlib\Map
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Stdlib\Map;
16
17use phpOMS\Utils\Permutation;
18
19/**
20 * Multimap utils.
21 *
22 * @package phpOMS\Stdlib\Map
23 * @license OMS License 2.0
24 * @link    https://jingga.app
25 * @since   1.0.0
26 */
27final class MultiMap implements \Countable
28{
29    /**
30     * Stored values.
31     *
32     * @var array<int|string, mixed>
33     * @since 1.0.0
34     */
35    private array $values = [];
36
37    /**
38     * Associated keys for values.
39     *
40     * @var array<int|string, int|string>
41     * @since 1.0.0
42     */
43    private array $keys = [];
44
45    /**
46     * Key type.
47     *
48     * @var int
49     * @since 1.0.0
50     */
51    private int $keyType = KeyType::SINGLE;
52
53    /**
54     * Order type.
55     *
56     * @var int
57     * @since 1.0.0
58     */
59    private int $orderType = OrderType::LOOSE;
60
61    /**
62     * Constructor.
63     *
64     * @param int $key   Key type (all keys need to match or just one)
65     * @param int $order Order of the keys is important (only required for multiple keys)
66     *
67     * @since 1.0.0
68     */
69    public function __construct(int $key = KeyType::SINGLE, int $order = OrderType::LOOSE)
70    {
71        $this->keyType   = $key;
72        $this->orderType = $order;
73    }
74
75    /**
76     * Add data.
77     *
78     * @param array<int|float|string> $keys      Keys for value
79     * @param mixed                   $value     Value to store
80     * @param bool                    $overwrite Add value if key exists
81     *
82     * @return bool
83     *
84     * @since 1.0.0
85     */
86    public function add(array $keys, mixed $value, bool $overwrite = false) : bool
87    {
88        $id        = \count($this->values);
89        $inserted  = false;
90        $keysBuild = $keys;
91
92        if ($this->keyType !== KeyType::SINGLE) {
93            $keysBuild = [\implode(':', $keysBuild)];
94
95            // prevent adding elements if keys are just ordered differently
96            if ($this->orderType === OrderType::LOOSE) {
97                /** @var array<string[]> $keysToTest */
98                $keysToTest = Permutation::permut($keys, [], false);
99
100                foreach ($keysToTest as $test) {
101                    $key = \implode(':', $test);
102
103                    if (isset($this->keys[$key])) {
104                        if (!$overwrite) {
105                            return false;
106                        }
107
108                        $keysBuild = [$key];
109                        break;
110                    }
111                }
112            }
113        }
114
115        foreach ($keysBuild as $key) {
116            if ($overwrite || !isset($this->keys[$key])) {
117                $id               = $this->keys[$key] ?? $id;
118                $this->keys[$key] = $id;
119
120                $inserted = true;
121            }
122        }
123
124        if ($inserted) {
125            $this->values[$id] = $value;
126        }
127
128        return $inserted;
129    }
130
131    /**
132     * Garbage collect unreferenced values/keys
133     *
134     * @return void
135     *
136     * @since 1.0.0
137     */
138    private function garbageCollect() : void
139    {
140        $this->garbageCollectKeys();
141        $this->garbageCollectValues();
142    }
143
144    /**
145     * Garbage collect unreferenced keys
146     *
147     * @return void
148     *
149     * @since 1.0.0
150     */
151    private function garbageCollectKeys() : void
152    {
153        foreach ($this->keys as $key => $keyValue) {
154            if (!isset($this->values[$keyValue])) {
155                unset($this->keys[$key]);
156            }
157        }
158    }
159
160    /**
161     * Garbage collect unreferenced values
162     *
163     * @return void
164     *
165     * @since 1.0.0
166     */
167    private function garbageCollectValues() : void
168    {
169        foreach ($this->values as $valueKey => $_) {
170            if (!\in_array($valueKey, $this->keys)) {
171                unset($this->values[$valueKey]);
172            }
173        }
174    }
175
176    /**
177     * Get data.
178     *
179     * @param int|string|array $key Key used to identify value
180     *
181     * @return mixed
182     *
183     * @since 1.0.0
184     */
185    public function get(int | string | array $key) : mixed
186    {
187        if ($this->keyType === KeyType::MULTIPLE || \is_array($key)) {
188            return $this->getMultiple($key);
189        }
190
191        return $this->getSingle($key);
192    }
193
194    /**
195     * Get data.
196     *
197     * @param int|string $key Key used to identify value
198     *
199     * @return mixed
200     *
201     * @since 1.0.0
202     */
203    private function getSingle(int | string $key) : mixed
204    {
205        return isset($this->keys[$key]) ? $this->values[$this->keys[$key]] ?? null : null;
206    }
207
208    /**
209     * Get data.
210     *
211     * @param int|string|array $key Key used to identify value
212     *
213     * @return mixed
214     *
215     * @since 1.0.0
216     */
217    private function getMultiple(int | string | array $key) : mixed
218    {
219        if (\is_array($key)) {
220            if ($this->orderType === OrderType::LOOSE) {
221                /** @var array<string[]> $keys */
222                $keys = Permutation::permut($key, [], false);
223
224                foreach ($keys as $key => $value) {
225                    $key = \implode(':', $value);
226
227                    if (isset($this->keys[$key])) {
228                        return $this->values[$this->keys[$key]];
229                    }
230                }
231            } else {
232                $key = \implode(':', $key);
233            }
234        }
235
236        return !\is_array($key) && isset($this->keys[$key])
237            ? $this->values[$this->keys[$key]] ?? null
238            : null;
239    }
240
241    /**
242     * Set existing key with data.
243     *
244     * @param int|string|array $key   Key used to identify value
245     * @param mixed            $value Value to store
246     *
247     * @return bool
248     *
249     * @since 1.0.0
250     */
251    public function set(int | string | array $key, mixed $value) : bool
252    {
253        if ($this->keyType === KeyType::MULTIPLE || \is_array($key)) {
254            return $this->setMultiple($key, $value);
255        }
256
257        return $this->setSingle($key, $value);
258    }
259
260    /**
261     * Set existing key with data.
262     *
263     * @param int|string|array $key   Key used to identify value
264     * @param mixed            $value Value to store
265     *
266     * @return bool
267     *
268     * @since 1.0.0
269     */
270    private function setMultiple(int | string | array $key, mixed $value) : bool
271    {
272        $key = \is_array($key) ? $key : [$key];
273
274        if ($this->orderType !== OrderType::STRICT) {
275            /** @var array<string[]> $permutation */
276            $permutation = Permutation::permut($key, [], false);
277
278            foreach ($permutation as $permut) {
279                if ($this->setSingle(\implode(':', $permut), $value)) {
280                    return true;
281                }
282            }
283
284            return false;
285        }
286
287        return $this->setSingle(\implode(':', $key), $value);
288    }
289
290    /**
291     * Set existing key with data.
292     *
293     * @param int|string $key   Key used to identify value
294     * @param mixed      $value Value to store
295     *
296     * @return bool
297     *
298     * @since 1.0.0
299     */
300    private function setSingle(int | string $key, mixed $value) : bool
301    {
302        if (isset($this->keys[$key])) {
303            $this->values[$this->keys[$key]] = $value;
304
305            return true;
306        }
307
308        return false;
309    }
310
311    /**
312     * Remove value and all sibling keys based on key.
313     *
314     * @param int|string|array $key Key used to identify value
315     *
316     * @return bool
317     *
318     * @since 1.0.0
319     */
320    public function remove(int | string | array $key) : bool
321    {
322        if ($this->keyType === KeyType::MULTIPLE || \is_array($key)) {
323            return $this->removeMultiple($key);
324        }
325
326        return $this->removeSingle($key);
327    }
328
329    /**
330     * Remove value and all sibling keys based on key.
331     *
332     * @param int|string|array $key Key used to identify value
333     *
334     * @return bool
335     *
336     * @since 1.0.0
337     */
338    private function removeMultiple(int | string | array $key) : bool
339    {
340        $key = \is_array($key) ? $key : [$key];
341
342        if ($this->orderType !== OrderType::LOOSE) {
343            return $this->removeSingle(\implode(':', $key));
344        }
345
346        /** @var array $keys */
347        $keys  = Permutation::permut($key, [], false);
348        $found = false;
349
350        foreach ($keys as $key => $value) {
351            $allFound = $this->removeSingle(\implode(':', $value));
352
353            if ($allFound) {
354                $found = true;
355            }
356        }
357
358        return $found;
359    }
360
361    /**
362     * Remove value and all sibling keys based on key.
363     *
364     * @param int|string $key Key used to identify value
365     *
366     * @return bool
367     *
368     * @since 1.0.0
369     */
370    private function removeSingle(int | string $key) : bool
371    {
372        if (isset($this->keys[$key])) {
373            $id = $this->keys[$key];
374
375            unset($this->values[$id]);
376
377            $this->garbageCollect();
378
379            return true;
380        }
381
382        return false;
383    }
384
385    /**
386     * Remap key to a different value.
387     *
388     * Both keys need to exist in the multimap.
389     *
390     * @param int|string $old Old key
391     * @param int|string $new New key
392     *
393     * @return bool
394     *
395     * @since 1.0.0
396     */
397    public function remap(int | string $old, int | string $new) : bool
398    {
399        if ($this->keyType === KeyType::MULTIPLE) {
400            return false;
401        }
402
403        if (isset($this->keys[$old], $this->keys[$new])) {
404            $this->keys[$old] = $this->keys[$new];
405
406            $this->garbageCollect();
407
408            return true;
409        }
410
411        return false;
412    }
413
414    /**
415     * Remove key.
416     *
417     * This only removes the value if no other key exists for this value.
418     *
419     * @param int|string $key Key used to identify value
420     *
421     * @return bool
422     *
423     * @since 1.0.0
424     */
425    public function removeKey(int | string $key) : bool
426    {
427        if ($this->keyType === KeyType::MULTIPLE) {
428            return false;
429        }
430
431        return $this->removeKeySingle($key);
432    }
433
434    /**
435     * Remove key.
436     *
437     * This only removes the value if no other key exists for this value.
438     *
439     * @param int|string $key Key used to identify value
440     *
441     * @return bool
442     *
443     * @since 1.0.0
444     */
445    private function removeKeySingle(int | string $key) : bool
446    {
447        if (isset($this->keys[$key])) {
448            unset($this->keys[$key]);
449
450            $this->garbageCollect();
451
452            return true;
453        }
454
455        return false;
456    }
457
458    /**
459     * Get all sibling keys.
460     *
461     * @param int|string|array $key Key to find siblings for
462     *
463     * @return array
464     *
465     * @since 1.0.0
466     */
467    public function getSiblings(int | string | array $key) : array
468    {
469        if ($this->keyType === KeyType::MULTIPLE || \is_array($key)) {
470            return $this->getSiblingsMultiple($key);
471        }
472
473        return $this->getSiblingsSingle($key);
474    }
475
476    /**
477     * Get all sibling keys.
478     *
479     * @param int|string|array $key Key to find siblings for
480     *
481     * @return array
482     *
483     * @since 1.0.0
484     */
485    public function getSiblingsMultiple(int | string | array $key) : array
486    {
487        if ($this->orderType === OrderType::LOOSE) {
488            $key = \is_array($key) ? $key : [$key];
489
490            return Permutation::permut($key, [], false);
491        }
492
493        return [];
494    }
495
496    /**
497     * Get all sibling keys.
498     *
499     * @param int|string $key Key to find siblings for
500     *
501     * @return array
502     *
503     * @since 1.0.0
504     */
505    private function getSiblingsSingle(int | string $key) : array
506    {
507        $siblings = [];
508        if (!isset($this->keys[$key])) {
509            return [];
510        }
511
512        $id = $this->keys[$key];
513
514        foreach ($this->keys as $found => $value) {
515            if ($value === $id && $found !== $key) {
516                $siblings[] = $found;
517            }
518        }
519
520        return $siblings;
521    }
522
523    /**
524     * Get all keys.
525     *
526     * @return array
527     *
528     * @since 1.0.0
529     */
530    public function keys() : array
531    {
532        return $this->keys;
533    }
534
535    /**
536     * Get all values.
537     *
538     * @return array
539     *
540     * @since 1.0.0
541     */
542    public function values() : array
543    {
544        return $this->values;
545    }
546
547    /**
548     * Count values.
549     *
550     * @return int
551     *
552     * @since 1.0.0
553     */
554    public function count() : int
555    {
556        return \count($this->values);
557    }
558}