Completed
Push — master ( e0916e...38b230 )
by Rudi
05:33
created

Map::contains()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 4
nop 2

2 Methods

Rating   Name   Duplication   Size   Complexity  
A Map::hasValue() 0 4 1
A Map::count() 0 4 1
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
    public function __construct($values = null)
32
    {
33
        if (func_num_args()) {
34
            $this->putAll($values);
0 ignored issues
show
Bug introduced by
It seems like $values defined by parameter $values on line 31 can also be of type null; however, Ds\Map::putAll() does only seem to accept object<Traversable>|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
35
        }
36
    }
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
    public function apply(callable $callback)
45
    {
46
        foreach ($this->internal as &$pair) {
47
            $pair->value = $callback($pair->key, $pair->value);
48
        }
49
    }
50
51
    /**
52
     * @inheritDoc
53
     */
54
    public function clear()
55
    {
56
        $this->internal = [];
57
        $this->capacity = self::MIN_CAPACITY;
58
    }
59
60
    /**
61
     * Return the first Pair from the Map
62
     *
63
     * @return Pair
64
     *
65
     * @throws UnderflowException
66
     */
67
    public function first(): Pair
68
    {
69
        if ($this->isEmpty()) {
70
            throw new UnderflowException();
71
        }
72
73
        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
    public function last(): Pair
84
    {
85
        if ($this->isEmpty()) {
86
            throw new UnderflowException();
87
        }
88
89
        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
    public function skip(int $position): Pair
102
    {
103
        if ($position < 0 || $position >= count($this->internal)) {
104
            throw new OutOfRangeException();
105
        }
106
107
        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
    public function merge($values): Map
118
    {
119
        $merged = new self($this);
120
        $merged->putAll($values);
121
122
        return $merged;
123
    }
124
125
    /**
126
     * Intersect
127
     *
128
     * @param Map $map
129
     *
130
     * @return Map
131
     */
132
    public function intersect(Map $map): Map
133
    {
134
        return $this->filter(function($key) use ($map) {
135
            return $map->hasKey($key);
136
        });
137
    }
138
139
    /**
140
     * Diff
141
     *
142
     * @param Map $map
143
     *
144
     * @return Map
145
     */
146
    public function diff(Map $map): Map
147
    {
148
        return $this->filter(function($key) use ($map) {
149
            return ! $map->hasKey($key);
150
        });
151
    }
152
153
    /**
154
     * Identical
155
     *
156
     * @param mixed $a
157
     * @param mixed $b
158
     *
159
     * @return bool
160
     */
161
    private function keysAreEqual($a, $b): bool
162
    {
163
        if (is_object($a) && $a instanceof Hashable) {
164
            return get_class($a) === get_class($b) && $a->equals($b);
165
        }
166
167
        return $a === $b;
168
    }
169
170
    /**
171
     * @param $key
172
     *
173
     * @return Pair|null
174
     */
175
    private function lookupKey($key)
176
    {
177
        foreach ($this->internal as $pair) {
178
            if ($this->keysAreEqual($pair->key, $key)) {
179
                return $pair;
180
            }
181
        }
182
    }
183
184
    /**
185
     * @param $value
186
     *
187
     * @return Pair|null
188
     */
189
    private function lookupValue($value)
190
    {
191
        foreach ($this->internal as $pair) {
192
            if ($pair->value === $value) {
193
                return $pair;
194
            }
195
        }
196
    }
197
198
    /**
199
     * Returns whether an association a given key exists.
200
     *
201
     * @param mixed $key
202
     *
203
     * @return bool
204
     */
205
    public function hasKey($key): bool
206
    {
207
        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
    public function hasValue($value): bool
218
    {
219
        return $this->lookupValue($value) !== null;
220
    }
221
222
    /**
223
     * @inheritDoc
224
     */
225
    public function count(): int
226
    {
227
        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
    public function filter(callable $callback = null): Map
241
    {
242
        $filtered = new self();
243
244
        foreach ($this as $key => $value) {
245
            if ($callback ? $callback($key, $value) : $value) {
246
                $filtered->put($key, $value);
247
            }
248
        }
249
250
        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
    public function get($key, $default = null)
266
    {
267
        if (($pair = $this->lookupKey($key))) {
268
            return $pair->value;
269
        }
270
271
        if (func_num_args() === 1) {
272
            throw new OutOfBoundsException();
273
        }
274
275
        return $default;
276
    }
277
278
    /**
279
     * Returns a set of all the keys in the map.
280
     *
281
     * @return Set
282
     */
283
    public function keys(): Set
284
    {
285
        $set = new Set();
286
287
        foreach ($this->internal as $pair) {
288
            $set->add($pair->key);
289
        }
290
291
        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
    public function map(callable $callback): Map
305
    {
306
        $mapped = new self();
307
308
        foreach ($this->internal as $pair) {
309
            $mapped[$pair->key] = $callback($pair->key, $pair->value);
310
        }
311
312
        return $mapped;
313
    }
314
315
    /**
316
     * Returns a sequence of pairs representing all associations.
317
     *
318
     * @return Sequence
319
     */
320
    public function pairs(): Sequence
321
    {
322
        $sequence = new Vector();
323
324
        foreach ($this->internal as $pair) {
325
            $sequence[] = $pair->copy();
326
        }
327
328
        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
    public function put($key, $value)
339
    {
340
        $pair = $this->lookupKey($key);
341
342
        if ($pair) {
343
            $pair->value = $value;
344
345
        } else {
346
            $this->adjustCapacity();
347
            $this->internal[] = new Pair($key, $value);
348
        }
349
    }
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
    public function putAll($values)
358
    {
359
        foreach ($values as $key => $value) {
360
            $this->put($key, $value);
361
        }
362
    }
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
    public function reduce(callable $callback, $initial = null)
376
    {
377
        $carry = $initial;
378
379
        foreach ($this->internal as $pair) {
380
            $carry = $callback($carry, $pair->key, $pair->value);
381
        }
382
383
        return $carry;
384
    }
385
386
    /**
387
     *
388
     */
389
    private function delete(int $position)
390
    {
391
        $pair  = $this->internal[$position];
392
        $value = $pair->value;
393
394
        array_splice($this->internal, $position, 1, null);
395
396
        $this->adjustCapacity();
397
        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
    public function remove($key, $default = null)
413
    {
414
        foreach ($this->internal as $position => $pair) {
415
            if ($this->keysAreEqual($pair->key, $key)) {
416
                return $this->delete($position);
417
            }
418
        }
419
420
        // Check if a default was provided
421
        if (func_num_args() === 1) {
422
            throw new \OutOfBoundsException();
423
        }
424
425
        return $default;
426
    }
427
428
    /**
429
     * Returns a reversed copy of the map.
430
     */
431
    public function reverse()
432
    {
433
        $this->internal = array_reverse($this->internal);
434
    }
435
436
    /**
437
     * Returns a reversed copy of the map.
438
     */
439
    public function reversed(): Map
440
    {
441
        $reversed = new self();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 11 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
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
    public function slice(int $offset, int $length = null): Map
471
    {
472
        $map = new self();
473
474
        if (func_num_args() === 1) {
475
            $slice = array_slice($this->internal, $offset);
476
        } else {
477
            $slice = array_slice($this->internal, $offset, $length);
478
        }
479
480
        foreach ($slice as $pair) {
481
            $map->put($pair->key, $pair->value);
482
        }
483
484
        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 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
        if ($comparator) {
497
            usort($this->internal, function($a, $b) use ($comparator) {
498
                return $comparator($a->value, $b->value);
499
            });
500
501
        } else {
502
            usort($this->internal, function($a, $b) {
503
                return $a->value <=> $b->value;
504
            });
505
        }
506
    }
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 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
        $sorted = new self($this);
519
520
        if ($comparator) {
521
            usort($sorted->internal, function($a, $b) use ($comparator) {
522
                return $comparator($a->value, $b->value);
523
            });
524
525
        } else {
526
            usort($sorted->internal, function($a, $b) {
527
                return $a->value <=> $b->value;
528
            });
529
        }
530
531
        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 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
        if ($comparator) {
544
            usort($this->internal, function($a, $b) use ($comparator) {
545
                return $comparator($a->key, $b->key);
546
            });
547
548
        } else {
549
            usort($this->internal, function($a, $b) {
550
                return $a->key <=> $b->key;
551
            });
552
        }
553
    }
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 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
        $sorted = $this->copy();
566
567
        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
                return $comparator($a->key, $b->key);
570
            });
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
                return $a->key <=> $b->key;
575
            });
576
        }
577
578
        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
    public function sum()
587
    {
588
        return $this->values()->sum();
589
    }
590
591
    /**
592
     * @inheritDoc
593
     */
594
    public function toArray(): array
595
    {
596
        $array = [];
597
598
        foreach ($this->internal as $pair) {
599
            $array[$pair->key] = $pair->value;
600
        }
601
602
        return $array;
603
    }
604
605
    /**
606
     * Returns a sequence of all the associated values in the Map.
607
     *
608
     * @return Sequence
609
     */
610
    public function values(): Sequence
611
    {
612
        $sequence = new Vector();
613
614
        foreach ($this->internal as $pair) {
615
            $sequence->push($pair->value);
616
        }
617
618
        return $sequence;
619
    }
620
621
    /**
622
     *
623
     *
624
     * @return \Ds\Map
625
     */
626
    public function union(Map $map): Map
627
    {
628
        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
        return $this->merge($map)->filter(function($key) use ($map) {
641
            return $this->hasKey($key) ^ $map->hasKey($key);
642
        });
643
    }
644
645
    /**
646
     * Get iterator
647
     */
648
    public function getIterator()
649
    {
650
        foreach ($this->internal as $pair) {
651
            yield $pair->key => $pair->value;
652
        }
653
    }
654
655
    /**
656
     * Debug Info
657
     */
658
    public function __debugInfo()
659
    {
660
        return $this->pairs()->toArray();
661
    }
662
663
    /**
664
     * @inheritdoc
665
     */
666
    public function offsetSet($offset, $value)
667
    {
668
        $this->put($offset, $value);
669
    }
670
671
    /**
672
     * @inheritdoc
673
     *
674
     * @throws OutOfBoundsException
675
     */
676
    public function &offsetGet($offset)
677
    {
678
        $pair = $this->lookupKey($offset);
679
680
        if ($pair) {
681
            return $pair->value;
682
        }
683
684
        throw new OutOfBoundsException();
685
    }
686
687
    /**
688
     * @inheritdoc
689
     */
690
    public function offsetUnset($offset)
691
    {
692
        $this->remove($offset, null);
693
    }
694
695
    /**
696
     * @inheritdoc
697
     */
698
    public function offsetExists($offset)
699
    {
700
        return $this->get($offset, null) !== null;
701
    }
702
}
703