Completed
Push — master ( 5856c5...90f7ba )
by Rudi
05:12
created

Map::filter()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 3
nop 1
crap 4
1
<?php
2
namespace Ds;
3
4
use OutOfBoundsException;
5
use OutOfRangeException;
6
use Traversable;
7
use UnderflowException;
8
9
/**
10
 * Class Map
11
 *
12
 * @package Ds
13
 */
14
final class Map implements \IteratorAggregate, \ArrayAccess, Collection
15
{
16
    use Traits\Collection;
17
    use Traits\SquaredCapacity;
18
19
    const MIN_CAPACITY = 8;
20
21
    /**
22
     * @var Pair[]
23
     */
24
    private $internal = [];
25
26
    /**
27
     * Creates an instance using the values of an array or Traversable object.
28
     *
29
     * @param array|\Traversable|null $values
30
     */
31 1018
    public function __construct($values = null)
32
    {
33 1018
        if (func_num_args()) {
34 525
            $this->putAll($values);
35
        }
36 1018
    }
37
38
    /**
39
     * Updates all values by applying a callback function to each value.
40
     *
41
     * @param callable $callback Accepts two arguments: key and value, should
42
     *                           return what the updated value will be.
43
     */
44 5
    public function apply(callable $callback)
45
    {
46 5
        foreach ($this->internal as &$pair) {
47 4
            $pair->value = $callback($pair->key, $pair->value);
48
        }
49 2
    }
50
51
    /**
52
     * @inheritDoc
53
     */
54 4
    public function clear()
55
    {
56 4
        $this->internal = [];
57 4
        $this->capacity = self::MIN_CAPACITY;
58 4
    }
59
60
    /**
61
     * Return the first Pair from the Map
62
     *
63
     * @return Pair
64
     *
65
     * @throws UnderflowException
66
     */
67 8
    public function first(): Pair
68
    {
69 8
        if ($this->isEmpty()) {
70 2
            throw new UnderflowException();
71
        }
72
73 6
        return $this->internal[0];
74
    }
75
76
    /**
77
     * Return the last Pair from the Map
78
     *
79
     * @return Pair
80
     *
81
     * @throws UnderflowException
82
     */
83 8
    public function last(): Pair
84
    {
85 8
        if ($this->isEmpty()) {
86 2
            throw new UnderflowException();
87
        }
88
89 6
        return end($this->internal);
90
    }
91
92
    /**
93
     * Return the pair at a specified position in the Map
94
     *
95
     * @param int $position
96
     *
97
     * @return Pair
98
     *
99
     * @throws OutOfRangeException
100
     */
101 37
    public function skip(int $position): Pair
102
    {
103 37
        if ($position < 0 || $position >= count($this->internal)) {
104 13
            throw new OutOfRangeException();
105
        }
106
107 24
        return $this->internal[$position]->copy();
108
    }
109
110
    /**
111
     * Merge an array of values with the current Map
112
     *
113
     * @param array|\Traversable $values
114
     *
115
     * @return Map
116
     */
117 44
    public function merge($values): Map
118
    {
119 44
        $merged = new self($this);
120 44
        $merged->putAll($values);
121
122 44
        return $merged;
123
    }
124
125
    /**
126
     * Intersect
127
     *
128
     * @param Map $map
129
     *
130
     * @return Map
131
     */
132 24
    public function intersect(Map $map): Map
133
    {
134
        return $this->filter(function($key) use ($map) {
135 20
            return $map->hasKey($key);
136 24
        });
137
    }
138
139
    /**
140
     * Diff
141
     *
142
     * @param Map $map
143
     *
144
     * @return Map
145
     */
146 24
    public function diff(Map $map): Map
147
    {
148
        return $this->filter(function($key) use ($map) {
149 20
            return ! $map->hasKey($key);
150 24
        });
151
    }
152
153
    /**
154
     * Identical
155
     *
156
     * @param mixed $a
157
     * @param mixed $b
158
     *
159
     * @return bool
160
     */
161 621
    private function keysAreEqual($a, $b): bool
162
    {
163 621
        if (is_object($a) && $a instanceof Hashable) {
164 18
            return get_class($a) === get_class($b) && $a->equals($b);
165
        }
166
167 617
        return $a === $b;
168
    }
169
170
    /**
171
     * @param $key
172
     *
173
     * @return Pair|null
174
     */
175 701
    private function lookupKey($key)
176
    {
177 701
        foreach ($this->internal as $pair) {
178 614
            if ($this->keysAreEqual($pair->key, $key)) {
179 614
                return $pair;
180
            }
181
        }
182 701
    }
183
184
    /**
185
     * @param $value
186
     *
187
     * @return Pair|null
188
     */
189 5
    private function lookupValue($value)
190
    {
191 5
        foreach ($this->internal as $pair) {
192 4
            if ($pair->value === $value) {
193 4
                return $pair;
194
            }
195
        }
196 3
    }
197
198
    /**
199
     * Returns whether an association a given key exists.
200
     *
201
     * @param mixed $key
202
     *
203
     * @return bool
204
     */
205 69
    public function hasKey($key): bool
206
    {
207 69
        return $this->lookupKey($key) !== null;
208
    }
209
210
    /**
211
     * Returns whether an association for a given value exists.
212
     *
213
     * @param mixed $value
214
     *
215
     * @return bool
216
     */
217 5
    public function hasValue($value): bool
218
    {
219 5
        return $this->lookupValue($value) !== null;
220
    }
221
222
    /**
223
     * @inheritDoc
224
     */
225 710
    public function count(): int
226
    {
227 710
        return count($this->internal);
228
    }
229
230
    /**
231
     * Returns a new map containing only the values for which a predicate
232
     * returns true. A boolean test will be used if a predicate is not provided.
233
     *
234
     * @param callable|null $callback Accepts a key and a value, and returns:
235
     *                                true : include the value,
236
     *                                false: skip the value.
237
     *
238
     * @return Map
239
     */
240 76
    public function filter(callable $callback = null): Map
241
    {
242 76
        $filtered = new self();
243
244 76
        foreach ($this as $key => $value) {
245 63
            if ($callback ? $callback($key, $value) : $value) {
246 62
                $filtered->put($key, $value);
247
            }
248
        }
249
250 73
        return $filtered;
251
    }
252
253
    /**
254
     * Returns the value associated with a key, or an optional default if the
255
     * key is not associated with a value.
256
     *
257
     * @param mixed $key
258
     * @param mixed $default
259
     *
260
     * @return mixed The associated value or fallback default if provided.
261
     *
262
     * @throws OutOfBoundsException if no default was provided and the key is
263
     *                               not associated with a value.
264
     */
265 33
    public function get($key, $default = null)
266
    {
267 33
        if (($pair = $this->lookupKey($key))) {
268 29
            return $pair->value;
269
        }
270
271 4
        if (func_num_args() === 1) {
272 1
            throw new OutOfBoundsException();
273
        }
274
275 3
        return $default;
276
    }
277
278
    /**
279
     * Returns a set of all the keys in the map.
280
     *
281
     * @return Set
282
     */
283 37
    public function keys(): Set
284
    {
285 37
        $set = new Set();
286
287 37
        foreach ($this->internal as $pair) {
288 19
            $set->add($pair->key);
289
        }
290
291 37
        return $set;
292
    }
293
294
    /**
295
     * Returns a new map using the results of applying a callback to each value.
296
     *
297
     * The keys will be equal in both maps.
298
     *
299
     * @param callable $callback Accepts two arguments: key and value, should
300
     *                           return what the updated value will be.
301
     *
302
     * @return Map
303
     */
304 5
    public function map(callable $callback): Map
305
    {
306 5
        $mapped = new self();
307
308 5
        foreach ($this->internal as $pair) {
309 4
            $mapped[$pair->key] = $callback($pair->key, $pair->value);
310
        }
311
312 2
        return $mapped;
313
    }
314
315
    /**
316
     * Returns a sequence of pairs representing all associations.
317
     *
318
     * @return Sequence
319
     */
320 7
    public function pairs(): Sequence
321
    {
322 7
        $sequence = new Vector();
323
324 7
        foreach ($this->internal as $pair) {
325 5
            $sequence[] = $pair->copy();
326
        }
327
328 7
        return $sequence;
329
    }
330
331
    /**
332
     * Associates a key with a value, replacing a previous association if there
333
     * was one.
334
     *
335
     * @param mixed $key
336
     * @param mixed $value
337
     */
338 693
    public function put($key, $value)
339
    {
340 693
        $pair = $this->lookupKey($key);
341
342 693
        if ($pair) {
343 64
            $pair->value = $value;
344
345
        } else {
346 693
            $this->adjustCapacity();
347 693
            $this->internal[] = new Pair($key, $value);
348
        }
349 693
    }
350
351
    /**
352
     * Creates associations for all keys and corresponding values of either an
353
     * array or iterable object.
354
     *
355
     * @param \Traversable|array $values
356
     */
357 525
    public function putAll($values)
358
    {
359 525
        foreach ($values as $key => $value) {
360 324
            $this->put($key, $value);
361
        }
362 525
    }
363
364
    /**
365
     * Iteratively reduces the map to a single value using a callback.
366
     *
367
     * @param callable $callback Accepts the carry, key, and value, and
368
     *                           returns an updated carry value.
369
     *
370
     * @param mixed|null $initial Optional initial carry value.
371
     *
372
     * @return mixed The carry value of the final iteration, or the initial
373
     *               value if the map was empty.
374
     */
375 8
    public function reduce(callable $callback, $initial = null)
376
    {
377 8
        $carry = $initial;
378
379 8
        foreach ($this->internal as $pair) {
380 6
            $carry = $callback($carry, $pair->key, $pair->value);
381
        }
382
383 6
        return $carry;
384
    }
385
386
    /**
387
     *
388
     */
389 28
    private function delete(int $position)
390
    {
391 28
        $pair  = $this->internal[$position];
392 28
        $value = $pair->value;
393
394 28
        array_splice($this->internal, $position, 1, null);
395
396 28
        $this->adjustCapacity();
397 28
        return $value;
398
    }
399
400
    /**
401
     * Removes a key's association from the map and returns the associated value
402
     * or a provided default if provided.
403
     *
404
     * @param mixed $key
405
     * @param mixed $default
406
     *
407
     * @return mixed The associated value or fallback default if provided.
408
     *
409
     * @throws \OutOfBoundsException if no default was provided and the key is
410
     *                               not associated with a value.
411
     */
412 34
    public function remove($key, $default = null)
413
    {
414 34
        foreach ($this->internal as $position => $pair) {
415 30
            if ($this->keysAreEqual($pair->key, $key)) {
416 30
                return $this->delete($position);
417
            }
418
        }
419
420
        // Check if a default was provided
421 9
        if (func_num_args() === 1) {
422 1
            throw new \OutOfBoundsException();
423
        }
424
425 8
        return $default;
426
    }
427
428
    /**
429
     * Returns a reversed copy of the map.
430
     */
431 15
    public function reverse()
432
    {
433 15
        $this->internal = array_reverse($this->internal);
434 15
    }
435
436
    /**
437
     * Returns a reversed copy of the map.
438
     */
439
    public function reversed(): Map
440
    {
441
        $reversed = new self();
442
        $reversed->internal = array_reverse($this->internal);
443
444
        return $reversed;
445
    }
446
447
    /**
448
     * Returns a sub-sequence of a given length starting at a specified offset.
449
     *
450
     * @param int $offset      If the offset is non-negative, the map will
451
     *                         start at that offset in the map. If offset is
452
     *                         negative, the map will start that far from the
453
     *                         end.
454
     *
455
     * @param int|null $length If a length is given and is positive, the
456
     *                         resulting set will have up to that many pairs in
457
     *                         it. If the requested length results in an
458
     *                         overflow, only pairs up to the end of the map
459
     *                         will be included.
460
     *
461
     *                         If a length is given and is negative, the map
462
     *                         will stop that many pairs from the end.
463
     *
464
     *                        If a length is not provided, the resulting map
465
     *                        will contains all pairs between the offset and
466
     *                        the end of the map.
467
     *
468
     * @return Map
469
     */
470 400
    public function slice(int $offset, int $length = null): Map
471
    {
472 400
        $map = new self();
473
474 400
        if (func_num_args() === 1) {
475 100
            $slice = array_slice($this->internal, $offset);
476
        } else {
477 300
            $slice = array_slice($this->internal, $offset, $length);
478
        }
479
480 400
        foreach ($slice as $pair) {
481 140
            $map->put($pair->key, $pair->value);
482
        }
483
484 400
        return $map;
485
    }
486
487
    /**
488
     * Sorts the map in-place, based on an optional callable comparator.
489
     *
490
     * The map will be sorted by value.
491
     *
492
     * @param callable|null $comparator Accepts two values to be compared.
493
     */
494 6 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...
495
    {
496 6
        if ($comparator) {
497
            usort($this->internal, function($a, $b) use ($comparator) {
498 2
                return $comparator($a->value, $b->value);
499 3
            });
500
501
        } else {
502
            usort($this->internal, function($a, $b) {
503 2
                return $a->value <=> $b->value;
504 3
            });
505
        }
506 6
    }
507
508
    /**
509
     * Returns a sorted copy of the map, based on an optional callable
510
     * comparator. The map will be sorted by value.
511
     *
512
     * @param callable|null $comparator Accepts two values to be compared.
513
     *
514
     * @return Map
515
     */
516 6 View Code Duplication
    public function sorted(callable $comparator = null): Map
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...
517
    {
518 6
        $sorted = new self($this);
519
520 6
        if ($comparator) {
521
            usort($sorted->internal, function($a, $b) use ($comparator) {
522 2
                return $comparator($a->value, $b->value);
523 3
            });
524
525
        } else {
526
            usort($sorted->internal, function($a, $b) {
527 2
                return $a->value <=> $b->value;
528 3
            });
529
        }
530
531 6
        return $sorted;
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 10 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 10
        if ($comparator) {
544
            usort($this->internal, function($a, $b) use ($comparator) {
545 4
                return $comparator($a->key, $b->key);
546 5
            });
547
548
        } else {
549
            usort($this->internal, function($a, $b) {
550 4
                return $a->key <=> $b->key;
551 5
            });
552
        }
553 10
    }
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 View Code Duplication
    public function ksorted(callable $comparator = null): Map
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...
564
    {
565 6
        $sorted = $this->copy();
566
567 6
        if ($comparator) {
568
            usort($sorted->internal, function($a, $b) use ($comparator) {
0 ignored issues
show
Bug introduced by
Accessing internal on the interface Ds\Collection suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
569 2
                return $comparator($a->key, $b->key);
570 3
            });
571
572
        } else {
573
            usort($sorted->internal, function($a, $b) {
0 ignored issues
show
Bug introduced by
Accessing internal on the interface Ds\Collection suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
574 2
                return $a->key <=> $b->key;
575 3
            });
576
        }
577
578 6
        return $sorted;
579
    }
580
581
    /**
582
     * Returns the sum of all values in the map.
583
     *
584
     * @return int|float The sum of all the values in the map.
585
     */
586 7
    public function sum()
587
    {
588 7
        return $this->values()->sum();
589
    }
590
591
    /**
592
     * @inheritDoc
593
     */
594 409
    public function toArray(): array
595
    {
596 409
        $array = [];
597
598 409
        foreach ($this->internal as $pair) {
599 252
            $array[$pair->key] = $pair->value;
600
        }
601
602 408
        return $array;
603
    }
604
605
    /**
606
     * Returns a sequence of all the associated values in the Map.
607
     *
608
     * @return Sequence
609
     */
610 10
    public function values(): Sequence
611
    {
612 10
        $sequence = new Vector();
613
614 10
        foreach ($this->internal as $pair) {
615 8
            $sequence->push($pair->value);
616
        }
617
618 10
        return $sequence;
619
    }
620
621
    /**
622
     *
623
     *
624
     * @return \Ds\Map
625
     */
626 12
    public function union(Map $map): Map
627
    {
628 12
        return $this->merge($map);
629
    }
630
631
    /**
632
     * XOR
633
     *
634
     * @param Map $map
635
     *
636
     * @return Map
637
     */
638
    public function xor(Map $map): Map
639
    {
640 20
        return $this->merge($map)->filter(function($key) use ($map) {
641 16
            return $this->hasKey($key) ^ $map->hasKey($key);
642 20
        });
643
    }
644
645
    /**
646
     * Get iterator
647
     */
648 535
    public function getIterator()
649
    {
650 535
        foreach ($this->internal as $pair) {
651 381
            yield $pair->key => $pair->value;
652
        }
653 532
    }
654
655
    /**
656
     * Debug Info
657
     */
658 3
    public function __debugInfo()
659
    {
660 3
        return $this->pairs()->toArray();
661
    }
662
663
    /**
664
     * @inheritdoc
665
     */
666 8
    public function offsetSet($offset, $value)
667
    {
668 8
        $this->put($offset, $value);
669 8
    }
670
671
    /**
672
     * @inheritdoc
673
     *
674
     * @throws OutOfBoundsException
675
     */
676 17
    public function &offsetGet($offset)
677
    {
678 17
        $pair = $this->lookupKey($offset);
679
680 17
        if ($pair) {
681 17
            return $pair->value;
682
        }
683
684
        throw new OutOfBoundsException();
685
    }
686
687
    /**
688
     * @inheritdoc
689
     */
690 4
    public function offsetUnset($offset)
691
    {
692 4
        $this->remove($offset, null);
693 4
    }
694
695
    /**
696
     * @inheritdoc
697
     */
698 20
    public function offsetExists($offset)
699
    {
700 20
        return $this->get($offset, null) !== null;
701
    }
702
}
703