Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
114 / 114 |
|
100.00% |
23 / 23 |
CRAP | |
100.00% |
1 / 1 |
MultiMap | |
100.00% |
114 / 114 |
|
100.00% |
23 / 23 |
71 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
add | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
10 | |||
garbageCollect | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
garbageCollectKeys | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
garbageCollectValues | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
get | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getSingle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getMultiple | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
7 | |||
set | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
setMultiple | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
setSingle | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
remove | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
removeMultiple | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
removeSingle | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
remap | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
removeKey | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
removeKeySingle | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getSiblings | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getSiblingsMultiple | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getSiblingsSingle | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
keys | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
values | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
count | |
100.00% |
1 / 1 |
|
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 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\Stdlib\Map; |
16 | |
17 | use 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 | */ |
27 | final 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 | } |