Completed
Push — master ( 7aefdd...988c00 )
by Rudi
02:20
created

Map::reversed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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