Completed
Push — master ( 988c00...ca3aab )
by Rudi
05:46
created

Map::apply()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 8
Ratio 100 %

Importance

Changes 0
Metric Value
dl 8
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 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 $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
     * 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 View Code Duplication
    public function apply(callable $callback): 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...
45
    {
46
        foreach ($this->pairs as &$pair) {
47
            $pair->value = $callback($pair->key, $pair->value);
48
        }
49
50
        return $mapped;
0 ignored issues
show
Bug introduced by
The variable $mapped does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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