Completed
Push — master ( 67857d...50814b )
by Rudi
02:55
created

Map   C

Complexity

Total Complexity 78

Size/Duplication

Total Lines 640
Duplicated Lines 9.38 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 78
c 0
b 0
f 0
lcom 1
cbo 7
dl 60
loc 640
rs 5.2842

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 4
A clear() 0 5 1
A removeAll() 0 6 2
A first() 0 8 2
A last() 0 8 2
A skip() 0 8 3
A merge() 0 7 1
A intersect() 0 6 1
A diff() 0 6 1
A xor() 0 6 1
A keysAreEqual() 0 8 3
A lookupKey() 0 8 3
A lookupValue() 0 8 3
A contains() 14 14 4
A hasKey() 0 4 1
A hasValue() 0 4 1
A count() 0 4 1
A filter() 12 12 4
A get() 0 12 3
A keys() 0 10 2
A map() 0 10 2
A pairs() 0 10 2
A put() 0 12 2
A putAll() 0 6 2
A reduce() 0 10 2
A delete() 0 10 1
A remove() 0 15 4
A reverse() 0 7 1
A slice() 0 16 3
A sort() 17 17 2
A ksort() 17 17 2
A toArray() 0 10 2
A values() 0 10 2
A getIterator() 0 6 2
A __debugInfo() 0 4 1
A offsetSet() 0 4 1
A offsetGet() 0 10 2
A offsetUnset() 0 4 1
A offsetExists() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Map often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Map, and based on these observations, apply Extract Interface, too.

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 $pairs;
25
26
    /**
27
     * Creates an instance using the values of an array or Traversable object.
28
     *
29
     * Should an integer be provided the Map will allocate the memory capacity
30
     * to the size of $values.
31
     *
32
     * @param array|\Traversable|int|null $values
33
     */
34
    public function __construct($values = null)
35
    {
36
        $this->pairs = [];
37
        $this->capacity = self::MIN_CAPACITY;
38
39
        if ($values && is_array($values) || $values instanceof Traversable) {
40
            $this->putAll($values);
41
        }
42
    }
43
44
    /**
45
     * @inheritDoc
46
     */
47
    public function clear()
48
    {
49
        $this->pairs = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 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...
50
        $this->capacity = self::MIN_CAPACITY;
51
    }
52
53
    /**
54
     * Removes all Pairs from the Map
55
     *
56
     * @param mixed[] $keys
57
     */
58
    public function removeAll($keys)
59
    {
60
        foreach ($keys as $key) {
61
            $this->remove($key);
62
        }
63
    }
64
65
    /**
66
     * Return the first Pair from the Map
67
     *
68
     * @return Pair
69
     *
70
     * @throws UnderflowException
71
     */
72
    public function first(): Pair
73
    {
74
        if ($this->isEmpty()) {
75
            throw new UnderflowException();
76
        }
77
78
        return $this->pairs[0];
79
    }
80
81
    /**
82
     * Return the last Pair from the Map
83
     *
84
     * @return Pair
85
     *
86
     * @throws UnderflowException
87
     */
88
    public function last(): Pair
89
    {
90
        if ($this->isEmpty()) {
91
            throw new UnderflowException();
92
        }
93
94
        return end($this->pairs);
95
    }
96
97
    /**
98
     * Return the pair at a specified position in the Map
99
     *
100
     * @param int $position
101
     *
102
     * @return Pair
103
     *
104
     * @throws OutOfRangeException
105
     */
106
    public function skip(int $position): Pair
107
    {
108
        if ($position < 0 || $position >= count($this->pairs)) {
109
            throw new OutOfRangeException();
110
        }
111
112
        return clone $this->pairs[$position];
113
    }
114
115
    /**
116
     * Merge an array of values with the current Map
117
     *
118
     * @param array|\Traversable $values
119
     *
120
     * @return Map
121
     */
122
    public function merge($values): Map
123
    {
124
        $merged = new self($this);
125
        $merged->putAll($values);
126
127
        return $merged;
128
    }
129
130
    /**
131
     * Intersect
132
     *
133
     * @param Map $map
134
     *
135
     * @return Map
136
     */
137
    public function intersect(Map $map): Map
138
    {
139
        return $this->filter(function($key) use ($map) {
140
            return $map->hasKey($key);
141
        });
142
    }
143
144
    /**
145
     * Diff
146
     *
147
     * @param Map $map
148
     *
149
     * @return Map
150
     */
151
    public function diff(Map $map): Map
152
    {
153
        return $this->filter(function($key) use ($map) {
154
            return ! $map->hasKey($key);
155
        });
156
    }
157
158
    /**
159
     * XOR
160
     *
161
     * @param Map $map
162
     *
163
     * @return Map
164
     */
165
    public function xor(Map $map): Map
166
    {
167
        return $this->merge($map)->filter(function($key) use ($map) {
168
            return $this->hasKey($key) ^ $map->hasKey($key);
169
        });
170
    }
171
172
    /**
173
     * Identical
174
     *
175
     * @param mixed $a
176
     * @param mixed $b
177
     *
178
     * @return bool
179
     */
180
    private function keysAreEqual($a, $b): bool
181
    {
182
        if (is_object($a) && $a instanceof Hashable) {
183
            return $a->equals($b);
184
        }
185
186
        return $a === $b;
187
    }
188
189
    /**
190
     * @param $key
191
     *
192
     * @return Pair|null
193
     */
194
    private function lookupKey($key)
195
    {
196
        foreach ($this->pairs as $pair) {
197
            if ($this->keysAreEqual($pair->key, $key)) {
198
                return $pair;
199
            }
200
        }
201
    }
202
203
    /**
204
     * @param $value
205
     *
206
     * @return Pair|null
207
     */
208
    private function lookupValue($value)
209
    {
210
        foreach ($this->pairs as $pair) {
211
            if ($pair->value === $value) {
212
                return $pair;
213
            }
214
        }
215
    }
216
217
    /**
218
     *
219
     */
220 View Code Duplication
    private function contains(string $lookup, array $values): bool
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...
221
    {
222
        if (empty($values)) {
223
            return false;
224
        }
225
226
        foreach ($values as $value) {
227
            if ( ! $this->$lookup($value)) {
228
                return false;
229
            }
230
        }
231
232
        return true;
233
    }
234
235
    /**
236
     * Returns whether an association for all of zero or more keys exist.
237
     *
238
     * @param mixed ...$keys
239
     *
240
     * @return bool true if at least one value was provided and the map
241
     *              contains all given keys, false otherwise.
242
     */
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $keys a bit more specific; maybe use array.
Loading history...
243
    public function hasKey(...$keys): bool
244
    {
245
        return $this->contains('lookupKey', $keys);
246
    }
247
248
    /**
249
     * Returns whether an association for all of zero or more values exist.
250
     *
251
     * @param mixed ...$values
252
     *
253
     * @return bool true if at least one value was provided and the map
254
     *              contains all given values, false otherwise.
255
     */
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $values a bit more specific; maybe use array.
Loading history...
256
    public function hasValue(...$values): bool
257
    {
258
        return $this->contains('lookupValue', $values);
259
    }
260
261
    /**
262
     * @inheritDoc
263
     */
264
    public function count(): int
265
    {
266
        return count($this->pairs);
267
    }
268
269
    /**
270
     * Returns a new map containing only the values for which a predicate
271
     * returns true. A boolean test will be used if a predicate is not provided.
272
     *
273
     * @param callable|null $predicate Accepts a key and a value, and returns:
274
     *                                 true : include the value,
275
     *                                 false: skip the value.
276
     *
277
     * @return Map
278
     */
279 View Code Duplication
    public function filter(callable $predicate = 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...
280
    {
281
        $filtered = new self();
282
283
        foreach ($this as $key => $value) {
284
            if ($predicate ? $predicate($key, $value) : $value) {
285
                $filtered->put($key, $value);
286
            }
287
        }
288
289
        return $filtered;
290
    }
291
292
    /**
293
     * Returns the value associated with a key, or an optional default if the
294
     * key is not associated with a value.
295
     *
296
     * @param mixed $key
297
     * @param mixed $default
298
     *
299
     * @return mixed The associated value or fallback default if provided.
300
     *
301
     * @throws OutOfBoundsException if no default was provided and the key is
302
     *                               not associated with a value.
303
     */
304
    public function get($key, $default = null)
305
    {
306
        if (($pair = $this->lookupKey($key))) {
307
            return $pair->value;
308
        }
309
310
        if (func_num_args() === 1) {
311
            throw new OutOfBoundsException();
312
        }
313
314
        return $default;
315
    }
316
317
    /**
318
     * Returns a set of all the keys in the map.
319
     *
320
     * @return Set
321
     */
322
    public function keys(): Set
323
    {
324
        $set = new Set();
325
326
        foreach ($this->pairs as $pair) {
327
            $set->add($pair->key);
328
        }
329
330
        return $set;
331
    }
332
333
    /**
334
     * Returns a new map using the results of applying a callback to each value.
335
     * The keys will be keysAreEqual in both maps.
336
     *
337
     * @param callable $callback Accepts two arguments: key and value, should
338
     *                           return what the updated value will be.
339
     *
340
     * @return Map
341
     */
342
    public function map(callable $callback): Map
343
    {
344
        $mapped = new self();
345
346
        foreach ($this->pairs as $pair) {
347
            $mapped[$pair->key] = $callback($pair->key, $pair->value);
348
        }
349
350
        return $mapped;
351
    }
352
353
    /**
354
     * Returns a sequence of pairs representing all associations.
355
     *
356
     * @return Sequence
357
     */
358
    public function pairs(): Sequence
359
    {
360
        $sequence = new Vector();
361
362
        foreach ($this->pairs as $pair) {
363
            $sequence[] = clone $pair;
364
        }
365
366
        return $sequence;
367
    }
368
369
    /**
370
     * Associates a key with a value, replacing a previous association if there
371
     * was one.
372
     *
373
     * @param mixed $key
374
     * @param mixed $value
375
     */
376
    public function put($key, $value)
377
    {
378
        $pair = $this->lookupKey($key);
379
380
        if ($pair) {
381
            $pair->value = $value;
382
383
        } else {
384
            $this->adjustCapacity();
385
            $this->pairs[] = new Pair($key, $value);
386
        }
387
    }
388
389
    /**
390
     * Creates associations for all keys and corresponding values of either an
391
     * array or iterable object.
392
     *
393
     * @param array|\Traversable $values
394
     */
395
    public function putAll($values)
396
    {
397
        foreach ($values as $key => $value) {
398
            $this->put($key, $value);
399
        }
400
    }
401
402
    /**
403
     * Iteratively reduces the map to a single value using a callback.
404
     *
405
     * @param callable $callback Accepts the carry, key, and value, and
406
     *                           returns an updated carry value.
407
     *
408
     * @param mixed|null $initial Optional initial carry value.
409
     *
410
     * @return mixed The carry value of the final iteration, or the initial
411
     *               value if the map was empty.
412
     */
413
    public function reduce(callable $callback, $initial = null)
414
    {
415
        $carry = $initial;
416
417
        foreach ($this->pairs as $pair) {
418
            $carry = $callback($carry, $pair->key, $pair->value);
419
        }
420
421
        return $carry;
422
    }
423
424
    private function delete(int $position)
425
    {
426
        $pair  = $this->pairs[$position];
427
        $value = $pair->value;
428
429
        array_splice($this->pairs, $position, 1, null);
430
431
        $this->adjustCapacity();
432
        return $value;
433
    }
434
435
    /**
436
     * Removes a key's association from the map and returns the associated value
437
     * or a provided default if provided.
438
     *
439
     * @param mixed $key
440
     * @param mixed $default
441
     *
442
     * @return mixed The associated value or fallback default if provided.
443
     *
444
     * @throws \OutOfBoundsException if no default was provided and the key is
445
     *                               not associated with a value.
446
     */
447
    public function remove($key, $default = null)
448
    {
449
        foreach ($this->pairs as $position => $pair) {
450
            if ($this->keysAreEqual($pair->key, $key)) {
451
                return $this->delete($position);
452
            }
453
        }
454
455
        // Check if a default was provided
456
        if (func_num_args() === 1) {
457
            throw new \OutOfBoundsException();
458
        }
459
460
        return $default;
461
    }
462
463
    /**
464
     * Returns a reversed copy of the map.
465
     */
466
    public function reverse(): Map
467
    {
468
        $reversed = new self();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 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...
469
        $reversed->pairs = array_reverse($this->pairs);
470
471
        return $reversed;
472
    }
473
474
    /**
475
     * Returns a sub-sequence of a given length starting at a specified offset.
476
     *
477
     * @param int $offset      If the offset is non-negative, the map will
478
     *                         start at that offset in the map. If offset is
479
     *                         negative, the map will start that far from the
480
     *                         end.
481
     *
482
     * @param int|null $length If a length is given and is positive, the
483
     *                         resulting set will have up to that many pairs in
484
     *                         it. If the requested length results in an
485
     *                         overflow, only pairs up to the end of the map
486
     *                         will be included.
487
     *
488
     *                         If a length is given and is negative, the map
489
     *                         will stop that many pairs from the end.
490
     *
491
     *                        If a length is not provided, the resulting map
492
     *                        will contains all pairs between the offset and
493
     *                        the end of the map.
494
     *
495
     * @return Map
496
     */
497
    public function slice(int $offset, int $length = null): Map
498
    {
499
        $map = new Map();
500
501
        if (func_num_args() === 1) {
502
            $slice = array_slice($this->pairs, $offset);
503
        } else {
504
            $slice = array_slice($this->pairs, $offset, $length);
505
        }
506
507
        foreach ($slice as $pair) {
508
            $map->put($pair->key, $pair->value);
509
        }
510
511
        return $map;
512
    }
513
514
    /**
515
     * Returns a sorted copy of the map, based on an optional callable
516
     * comparator. The map will be sorted by value.
517
     *
518
     * @param callable|null $comparator Accepts two values to be compared.
519
     *
520
     * @return Map
521
     */
522 View Code Duplication
    public function sort(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...
523
    {
524
        $sorted = new self($this);
525
526
        if ($comparator) {
527
            usort($sorted->pairs, function($a, $b) use ($comparator) {
528
                return $comparator($a->value, $b->value);
529
            });
530
531
        } else {
532
            usort($sorted->pairs, function($a, $b) {
533
                return $a->value <=> $b->value;
534
            });
535
        }
536
537
        return $sorted;
538
    }
539
540
    /**
541
     * Returns a sorted copy of the map, based on an optional callable
542
     * comparator. The map will be sorted by key.
543
     *
544
     * @param callable|null $comparator Accepts two keys to be compared.
545
     *
546
     * @return Map
547
     */
548 View Code Duplication
    public function ksort(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...
549
    {
550
        $sorted = clone $this;
551
552
        if ($comparator) {
553
            usort($sorted->pairs, function($a, $b) use ($comparator) {
554
                return $comparator($a->key, $b->key);
555
            });
556
557
        } else {
558
            usort($sorted->pairs, function($a, $b) {
559
                return $a->key <=> $b->key;
560
            });
561
        }
562
563
        return $sorted;
564
    }
565
566
    /**
567
     * @inheritDoc
568
     */
569
    public function toArray(): array
570
    {
571
        $array = [];
572
573
        foreach ($this->pairs as $pair) {
574
            $array[$pair->key] = $pair->value;
575
        }
576
577
        return $array;
578
    }
579
580
    /**
581
     * Returns a sequence of all the associated values in the Map.
582
     *
583
     * @return Sequence
584
     */
585
    public function values(): Sequence
586
    {
587
        $sequence = new Vector();
588
589
        foreach ($this->pairs as $pair) {
590
            $sequence->push($pair->value);
591
        }
592
593
        return $sequence;
594
    }
595
596
    /**
597
     * Get iterator
598
     */
599
    public function getIterator()
600
    {
601
        foreach ($this->pairs as $pair) {
602
            yield $pair->key => $pair->value;
603
        }
604
    }
605
606
    /**
607
     * Debug Info
608
     */
609
    public function __debugInfo()
610
    {
611
        return $this->pairs()->toArray();
612
    }
613
614
    /**
615
     * @inheritdoc
616
     */
617
    public function offsetSet($offset, $value)
618
    {
619
        $this->put($offset, $value);
620
    }
621
622
    /**
623
     * @inheritdoc
624
     *
625
     * @throws OutOfBoundsException
626
     */
627
    public function &offsetGet($offset)
628
    {
629
        $pair = $this->lookupKey($offset);
630
631
        if ($pair) {
632
            return $pair->value;
633
        }
634
635
        throw new OutOfBoundsException();
636
    }
637
638
    /**
639
     * @inheritdoc
640
     */
641
    public function offsetUnset($offset)
642
    {
643
        $this->remove($offset, null);
644
    }
645
646
    /**
647
     * @inheritdoc
648
     */
649
    public function offsetExists($offset)
650
    {
651
        return $this->get($offset, null) !== null;
652
    }
653
}
654