Completed
Push — master ( 914c2c...907858 )
by Rudi
03:08
created

Map::values()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
crap 1
1
<?php
2
namespace Ds;
3
4
use OutOfBoundsException;
5
use OutOfRangeException;
6
use Traversable;
7
use UnderflowException;
8
9
/**
10
 * A Map is a sequential collection of key-value pairs, almost identical to an
11
 * array used in a similar context. Keys can be any type, but must be unique.
12
 *
13
 * @package Ds
14
 */
15
final class Map implements \IteratorAggregate, \ArrayAccess, Collection
16
{
17
    use Traits\GenericCollection;
18
    use Traits\SquaredCapacity;
19
20
    const MIN_CAPACITY = 8;
21
22
    /**
23
     * @var array internal array to store pairs
24
     */
25
    private $pairs = [];
26
27
    /**
28
     * Creates a new instance.
29
     *
30
     * @param array|\Traversable|null $values
31
     */
32 1024
    public function __construct($values = null)
33
    {
34 1024
        if (func_num_args()) {
35 531
            $this->putAll($values);
36
        }
37 1024
    }
38
39
    /**
40
     * Updates all values by applying a callback function to each value.
41
     *
42
     * @param callable $callback Accepts two arguments: key and value, should
43
     *                           return what the updated value will be.
44
     */
45 5
    public function apply(callable $callback)
46
    {
47 5
        foreach ($this->pairs as &$pair) {
48 4
            $pair->value = $callback($pair->key, $pair->value);
49
        }
50 2
    }
51
52
    /**
53
     * @inheritDoc
54
     */
55 4
    public function clear()
56
    {
57 4
        $this->pairs = [];
58 4
        $this->capacity = self::MIN_CAPACITY;
59 4
    }
60
61
    /**
62
     * Return the first Pair from the Map
63
     *
64
     * @return Pair
65
     *
66
     * @throws UnderflowException
67
     */
68 8
    public function first(): Pair
69
    {
70 8
        if ($this->isEmpty()) {
71 2
            throw new UnderflowException();
72
        }
73
74 6
        return $this->pairs[0];
75
    }
76
77
    /**
78
     * Return the last Pair from the Map
79
     *
80
     * @return Pair
81
     *
82
     * @throws UnderflowException
83
     */
84 8
    public function last(): Pair
85
    {
86 8
        if ($this->isEmpty()) {
87 2
            throw new UnderflowException();
88
        }
89
90 6
        return $this->pairs[count($this->pairs) - 1];
91
    }
92
93
    /**
94
     * Return the pair at a specified position in the Map
95
     *
96
     * @param int $position
97
     *
98
     * @return Pair
99
     *
100
     * @throws OutOfRangeException
101
     */
102 37
    public function skip(int $position): Pair
103
    {
104 37
        if ($position < 0 || $position >= count($this->pairs)) {
105 13
            throw new OutOfRangeException();
106
        }
107
108 24
        return $this->pairs[$position]->copy();
109
    }
110
111
    /**
112
     * Merge an array of values with the current Map
113
     *
114
     * @param array|\Traversable $values
115
     *
116
     * @return Map
117
     */
118 44
    public function merge($values): Map
119
    {
120 44
        $merged = new self($this);
121 44
        $merged->putAll($values);
122 44
        return $merged;
123
    }
124
125
    /**
126
     * Creates a new map containing the pairs of the current instance whose keys
127
     * are also present in the given map. In other words, returns a copy of the
128
     * current map with all keys removed that are not also in the other map.
129
     *
130
     * @param Map $map The other map.
131
     *
132
     * @return Map A new map containing the pairs of the current instance
133
     *                 whose keys are also present in the given map. In other
134
     *                 words, returns a copy of the current map with all keys
135
     *                 removed that are not also in the other map.
136
     */
137 24
    public function intersect(Map $map): Map
138
    {
139
        return $this->filter(function($key) use ($map) {
140 20
            return $map->hasKey($key);
141 24
        });
142
    }
143
144
    /**
145
     * Returns the result of removing all keys from the current instance that
146
     * are present in a given map.
147
     *
148
     * @param Map $map The map containing the keys to exclude.
149
     *
150
     * @return Map The result of removing all keys from the current instance
151
     *                 that are present in a given map.
152
     */
153 24
    public function diff(Map $map): Map
154
    {
155
        return $this->filter(function($key) use ($map) {
156 20
            return ! $map->hasKey($key);
157 24
        });
158
    }
159
160
    /**
161
     * Determines whether two keys are equal.
162
     *
163
     * @param mixed $a
164
     * @param mixed $b
165
     *
166
     * @return bool
167
     */
168 625
    private function keysAreEqual($a, $b): bool
169
    {
170 625
        if (is_object($a) && $a instanceof Hashable) {
171 18
            return get_class($a) === get_class($b) && $a->equals($b);
172
        }
173
174 621
        return $a === $b;
175
    }
176
177
    /**
178
     * Attempts to look up a key in the table.
179
     *
180
     * @param $key
181
     *
182
     * @return Pair|null
183
     */
184 706
    private function lookupKey($key)
185
    {
186 706
        foreach ($this->pairs as $pair) {
187 618
            if ($this->keysAreEqual($pair->key, $key)) {
188 618
                return $pair;
189
            }
190
        }
191 706
    }
192
193
    /**
194
     * Attempts to look up a key in the table.
195
     *
196
     * @param $value
197
     *
198
     * @return Pair|null
199
     */
200 5
    private function lookupValue($value)
201
    {
202 5
        foreach ($this->pairs as $pair) {
203 4
            if ($pair->value === $value) {
204 4
                return $pair;
205
            }
206
        }
207 3
    }
208
209
    /**
210
     * Returns whether an association a given key exists.
211
     *
212
     * @param mixed $key
213
     *
214
     * @return bool
215
     */
216 69
    public function hasKey($key): bool
217
    {
218 69
        return $this->lookupKey($key) !== null;
219
    }
220
221
    /**
222
     * Returns whether an association for a given value exists.
223
     *
224
     * @param mixed $value
225
     *
226
     * @return bool
227
     */
228 5
    public function hasValue($value): bool
229
    {
230 5
        return $this->lookupValue($value) !== null;
231
    }
232
233
    /**
234
     * @inheritDoc
235
     */
236 715
    public function count(): int
237
    {
238 715
        return count($this->pairs);
239
    }
240
241
    /**
242
     * Returns a new map containing only the values for which a predicate
243
     * returns true. A boolean test will be used if a predicate is not provided.
244
     *
245
     * @param callable|null $callback Accepts a key and a value, and returns:
246
     *                                true : include the value,
247
     *                                false: skip the value.
248
     *
249
     * @return Map
250
     */
251 76
    public function filter(callable $callback = null): Map
252
    {
253 76
        $filtered = new self();
254
255 76
        foreach ($this as $key => $value) {
256 63
            if ($callback ? $callback($key, $value) : $value) {
257 62
                $filtered->put($key, $value);
258
            }
259
        }
260
261 73
        return $filtered;
262
    }
263
264
    /**
265
     * Returns the value associated with a key, or an optional default if the
266
     * key is not associated with a value.
267
     *
268
     * @param mixed $key
269
     * @param mixed $default
270
     *
271
     * @return mixed The associated value or fallback default if provided.
272
     *
273
     * @throws OutOfBoundsException if no default was provided and the key is
274
     *                               not associated with a value.
275
     */
276 33
    public function get($key, $default = null)
277
    {
278 33
        if (($pair = $this->lookupKey($key))) {
279 29
            return $pair->value;
280
        }
281
282
        // Check if a default was provided.
283 4
        if (func_num_args() === 1) {
284 1
            throw new OutOfBoundsException();
285
        }
286
287 3
        return $default;
288
    }
289
290
    /**
291
     * Returns a set of all the keys in the map.
292
     *
293
     * @return Set
294
     */
295 37
    public function keys(): Set
296
    {
297
        $key = function($pair) {
298 19
            return $pair->key;
299 37
        };
300
301 37
        return new Set(array_map($key, $this->pairs));
302
    }
303
304
    /**
305
     * Returns a new map using the results of applying a callback to each value.
306
     *
307
     * The keys will be equal in both maps.
308
     *
309
     * @param callable $callback Accepts two arguments: key and value, should
310
     *                           return what the updated value will be.
311
     *
312
     * @return Map
313
     */
314 5
    public function map(callable $callback): Map
315
    {
316
        $apply = function($pair) use ($callback) {
317 4
            return $callback($pair->key, $pair->value);
318 5
        };
319
320 5
        return new self(array_map($apply, $this->pairs));
321
    }
322
323
    /**
324
     * Returns a sequence of pairs representing all associations.
325
     *
326
     * @return Sequence
327
     */
328 7
    public function pairs(): Sequence
329
    {
330
        $copy = function($pair) {
331 5
            return $pair->copy();
332 7
        };
333
334 7
        return new Vector(array_map($copy, $this->pairs));
335
    }
336
337
    /**
338
     * Associates a key with a value, replacing a previous association if there
339
     * was one.
340
     *
341
     * @param mixed $key
342
     * @param mixed $value
343
     */
344 698
    public function put($key, $value)
345
    {
346 698
        $pair = $this->lookupKey($key);
347
348 698
        if ($pair) {
349 64
            $pair->value = $value;
350
351
        } else {
352 698
            $this->adjustCapacity();
353 698
            $this->pairs[] = new Pair($key, $value);
354
        }
355 698
    }
356
357
    /**
358
     * Creates associations for all keys and corresponding values of either an
359
     * array or iterable object.
360
     *
361
     * @param \Traversable|array $values
362
     */
363 531
    public function putAll($values)
364
    {
365 531
        foreach ($values as $key => $value) {
366 329
            $this->put($key, $value);
367
        }
368 531
    }
369
370
    /**
371
     * Iteratively reduces the map to a single value using a callback.
372
     *
373
     * @param callable $callback Accepts the carry, key, and value, and
374
     *                           returns an updated carry value.
375
     *
376
     * @param mixed|null $initial Optional initial carry value.
377
     *
378
     * @return mixed The carry value of the final iteration, or the initial
379
     *               value if the map was empty.
380
     */
381 8
    public function reduce(callable $callback, $initial = null)
382
    {
383 8
        $carry = $initial;
384
385 8
        foreach ($this->pairs as $pair) {
386 6
            $carry = $callback($carry, $pair->key, $pair->value);
387
        }
388
389 6
        return $carry;
390
    }
391
392
    /**
393
     * Completely removes a pair from the internal array by position. It is
394
     * important to remove it from the array and not just use 'unset'.
395
     */
396 28
    private function delete(int $position)
397
    {
398 28
        $pair  = $this->pairs[$position];
399 28
        $value = $pair->value;
400
401 28
        array_splice($this->pairs, $position, 1, null);
402 28
        $this->adjustCapacity();
403
404 28
        return $value;
405
    }
406
407
    /**
408
     * Removes a key's association from the map and returns the associated value
409
     * or a provided default if provided.
410
     *
411
     * @param mixed $key
412
     * @param mixed $default
413
     *
414
     * @return mixed The associated value or fallback default if provided.
415
     *
416
     * @throws \OutOfBoundsException if no default was provided and the key is
417
     *                               not associated with a value.
418
     */
419 34
    public function remove($key, $default = null)
420
    {
421 34
        foreach ($this->pairs as $position => $pair) {
422 30
            if ($this->keysAreEqual($pair->key, $key)) {
423 30
                return $this->delete($position);
424
            }
425
        }
426
427
        // Check if a default was provided
428 9
        if (func_num_args() === 1) {
429 1
            throw new \OutOfBoundsException();
430
        }
431
432 8
        return $default;
433
    }
434
435
    /**
436
     * Returns a reversed copy of the map.
437
     *
438
     * @return Map
0 ignored issues
show
Documentation introduced by
Should the return type not be Map|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
439
     */
440 15
    public function reverse()
441
    {
442 15
        $this->pairs = array_reverse($this->pairs);
443 15
    }
444
445
    /**
446
     * Returns a reversed copy of the map.
447
     *
448
     * @return Map
449
     */
450 5
    public function reversed(): Map
451
    {
452 5
        $reversed = new self();
453 5
        $reversed->pairs = array_reverse($this->pairs);
454
455 5
        return $reversed;
456
    }
457
458
    /**
459
     * Returns a sub-sequence of a given length starting at a specified offset.
460
     *
461
     * @param int $offset      If the offset is non-negative, the map will
462
     *                         start at that offset in the map. If offset is
463
     *                         negative, the map will start that far from the
464
     *                         end.
465
     *
466
     * @param int|null $length If a length is given and is positive, the
467
     *                         resulting set will have up to that many pairs in
468
     *                         it. If the requested length results in an
469
     *                         overflow, only pairs up to the end of the map
470
     *                         will be included.
471
     *
472
     *                         If a length is given and is negative, the map
473
     *                         will stop that many pairs from the end.
474
     *
475
     *                        If a length is not provided, the resulting map
476
     *                        will contains all pairs between the offset and
477
     *                        the end of the map.
478
     *
479
     * @return Map
480
     */
481 400
    public function slice(int $offset, int $length = null): Map
482
    {
483 400
        $map = new self();
484
485 400
        if (func_num_args() === 1) {
486 100
            $slice = array_slice($this->pairs, $offset);
487
        } else {
488 300
            $slice = array_slice($this->pairs, $offset, $length);
489
        }
490
491 400
        foreach ($slice as $pair) {
492 140
            $map->put($pair->key, $pair->value);
493
        }
494
495 400
        return $map;
496
    }
497
498
    /**
499
     * Sorts the map in-place, based on an optional callable comparator.
500
     *
501
     * The map will be sorted by value.
502
     *
503
     * @param callable|null $comparator Accepts two values to be compared.
504
     */
505 12 View Code Duplication
    public function sort(callable $comparator = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
506
    {
507 12
        if ($comparator) {
508
            usort($this->pairs, function($a, $b) use ($comparator) {
509 4
                return $comparator($a->value, $b->value);
510 6
            });
511
512
        } else {
513
            usort($this->pairs, function($a, $b) {
514 4
                return $a->value <=> $b->value;
515 6
            });
516
        }
517 12
    }
518
519
    /**
520
     * Returns a sorted copy of the map, based on an optional callable
521
     * comparator. The map will be sorted by value.
522
     *
523
     * @param callable|null $comparator Accepts two values to be compared.
524
     *
525
     * @return Map
526
     */
527 6
    public function sorted(callable $comparator = null): Map
528
    {
529 6
        $copy = $this->copy();
530 6
        $copy->sort($comparator);
531 6
        return $copy;
532
    }
533
534
    /**
535
     * Sorts the map in-place, based on an optional callable comparator.
536
     *
537
     * The map will be sorted by key.
538
     *
539
     * @param callable|null $comparator Accepts two keys to be compared.
540
     */
541 16 View Code Duplication
    public function ksort(callable $comparator = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
542
    {
543 16
        if ($comparator) {
544
            usort($this->pairs, function($a, $b) use ($comparator) {
545 6
                return $comparator($a->key, $b->key);
546 8
            });
547
548
        } else {
549
            usort($this->pairs, function($a, $b) {
550 6
                return $a->key <=> $b->key;
551 8
            });
552
        }
553 16
    }
554
555
    /**
556
     * Returns a sorted copy of the map, based on an optional callable
557
     * comparator. The map will be sorted by key.
558
     *
559
     * @param callable|null $comparator Accepts two keys to be compared.
560
     *
561
     * @return Map
562
     */
563 6
    public function ksorted(callable $comparator = null): Map
564
    {
565 6
        $copy = $this->copy();
566 6
        $copy->ksort($comparator);
567 6
        return $copy;
568
    }
569
570
    /**
571
     * Returns the sum of all values in the map.
572
     *
573
     * @return int|float The sum of all the values in the map.
574
     */
575 7
    public function sum()
576
    {
577 7
        return $this->values()->sum();
578
    }
579
580
    /**
581
     * @inheritDoc
582
     */
583 414
    public function toArray(): array
584
    {
585 414
        $array = [];
586
587 414
        foreach ($this->pairs as $pair) {
588 256
            $array[$pair->key] = $pair->value;
589
        }
590
591 413
        return $array;
592
    }
593
594
    /**
595
     * Returns a sequence of all the associated values in the Map.
596
     *
597
     * @return Sequence
598
     */
599 10
    public function values(): Sequence
600
    {
601
        $value = function($pair) {
602 8
            return $pair->value;
603 10
        };
604
605 10
        return new Vector(array_map($value, $this->pairs));
606
    }
607
608
    /**
609
     * Creates a new map that contains the pairs of the current instance as well
610
     * as the pairs of another map.
611
     *
612
     * @param Map $map The other map, to combine with the current instance.
613
     *
614
     * @return Map A new map containing all the pairs of the current
615
     *                 instance as well as another map.
616
     */
617 12
    public function union(Map $map): Map
618
    {
619 12
        return $this->merge($map);
620
    }
621
622
    /**
623
     * Creates a new map using keys of either the current instance or of another
624
     * map, but not of both.
625
     *
626
     * @param Map $map
627
     *
628
     * @return Map A new map containing keys in the current instance as well
629
     *                 as another map, but not in both.
630
     */
631
    public function xor(Map $map): Map
632
    {
633 20
        return $this->merge($map)->filter(function($key) use ($map) {
634 16
            return $this->hasKey($key) ^ $map->hasKey($key);
635 20
        });
636
    }
637
638
    /**
639
     * @inheritDoc
640
     */
641 535
    public function getIterator()
642
    {
643 535
        foreach ($this->pairs as $pair) {
644 381
            yield $pair->key => $pair->value;
645
        }
646 532
    }
647
648
    /**
649
     * Returns a representation to be used for var_dump and print_r.
650
     */
651 3
    public function __debugInfo()
652
    {
653 3
        return $this->pairs()->toArray();
654
    }
655
656
    /**
657
     * @inheritdoc
658
     */
659 5
    public function offsetSet($offset, $value)
660
    {
661 5
        $this->put($offset, $value);
662 5
    }
663
664
    /**
665
     * @inheritdoc
666
     *
667
     * @throws OutOfBoundsException
668
     */
669 18
    public function &offsetGet($offset)
670
    {
671 18
        $pair = $this->lookupKey($offset);
672
673 18
        if ($pair) {
674 17
            return $pair->value;
675
        }
676
677 1
        throw new OutOfBoundsException();
678
    }
679
680
    /**
681
     * @inheritdoc
682
     */
683 4
    public function offsetUnset($offset)
684
    {
685 4
        $this->remove($offset, null);
686 4
    }
687
688
    /**
689
     * @inheritdoc
690
     */
691 20
    public function offsetExists($offset)
692
    {
693 20
        return $this->get($offset, null) !== null;
694
    }
695
}
696