Completed
Push — master ( d5a821...790b7c )
by Rudi
02:20
created

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