Completed
Push — master ( 867177...7482d8 )
by Rudi
02:19
created

Map::merge()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

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