Completed
Pull Request — master (#43)
by
unknown
08:27
created

Map   C

Complexity

Total Complexity 74

Size/Duplication

Total Lines 682
Duplicated Lines 3.81 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 74
lcom 1
cbo 7
dl 26
loc 682
ccs 0
cts 294
cp 0
rs 5.1032
c 0
b 0
f 0

43 Methods

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