Completed
Push — master ( 8c94ac...c34064 )
by Rudi
02:16
created

Map::filter()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 15
rs 9.2
cc 4
eloc 8
nc 3
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, $value) use ($map) {
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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, $value) use ($map) {
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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, $value) use ($map) {
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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
        $pair = null;
211
212
        return $pair;
213
    }
214
215
    /**
216
     * Returns whether an association for all of zero or more keys exist.
217
     *
218
     * @param mixed ...$keys
219
     *
220
     * @return bool true if at least one value was provided and the map
221
     *              contains all given keys, false otherwise.
222
     */
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $keys a bit more specific; maybe use array.
Loading history...
223
    public function containsKey(...$keys): bool
224
    {
225
        if (empty($keys)) {
226
            return false;
227
        }
228
229
        foreach ($keys as $key) {
230
            if ( ! $this->lookup($key)) {
231
                return false;
232
            }
233
        }
234
235
        return true;
236
    }
237
238
    /**
239
     * Returns whether an association for all of zero or more values exist.
240
     *
241
     * @param mixed ...$values
242
     *
243
     * @return bool true if at least one value was provided and the map
244
     *              contains all given values, false otherwise.
245
     */
0 ignored issues
show
Documentation introduced by
Consider making the type for parameter $values a bit more specific; maybe use array.
Loading history...
246
    public function containsValue(...$values): bool
247
    {
248
        if (empty($values)) {
249
            return false;
250
        }
251
252
        foreach ($values as $value) {
253
            foreach ($this->pairs as $pair) {
254
                if ($pair->value === $value) {
255
                    continue 2;
256
                }
257
            }
258
259
            return false;
260
        }
261
262
        return true;
263
    }
264
265
    /**
266
     * @inheritDoc
267
     */
268
    public function copy()
269
    {
270
        return new self($this);
271
    }
272
273
    /**
274
     * @inheritDoc
275
     */
276
    public function count(): int
277
    {
278
        return count($this->pairs);
279
    }
280
281
    /**
282
     * Returns a new map containing only the values for which a callback
283
     * returns true. A boolean test will be used if a callback is not provided.
284
     *
285
     * @param callable|null $callback Accepts a key and a value, and returns:
286
     *                                true : include the value,
287
     *                                false: skip the value.
288
     *
289
     * @return Map
290
     */
291
    public function filter(callable $callback = null): Map
292
    {
293
        $filtered = new self();
294
295
        foreach ($this->pairs as $pair) {
296
            $k = $pair->key;
297
            $v = $pair->value;
298
299
            if ($callback ? $callback($k, $v) : $v) {
300
                $filtered->put($k, $v);
301
            }
302
        }
303
304
        return $filtered;
305
    }
306
307
    /**
308
     * Returns the value associated with a key, or an optional default if the
309
     * key is not associated with a value.
310
     *
311
     * @param mixed $key
312
     * @param mixed $default
313
     *
314
     * @return mixed The associated value or fallback default if provided.
315
     *
316
     * @throws OutOfBoundsException if no default was provided and the key is
317
     *                               not associated with a value.
318
     */
319
    public function get($key, $default = null)
320
    {
321
        if (($pair = $this->lookup($key))) {
322
            return $pair->value;
323
        }
324
325
        if (func_num_args() === 1) {
326
            throw new OutOfBoundsException();
327
        }
328
329
        return $default;
330
    }
331
332
    /**
333
     * Returns a set of all the keys in the map.
334
     *
335
     * @return Set
336
     */
337
    public function keys(): Set
338
    {
339
        $set = new Set();
340
341
        foreach ($this->pairs as $pair) {
342
            $set->add($pair->key);
343
        }
344
345
        return $set;
346
    }
347
348
    /**
349
     * Returns a new map using the results of applying a callback to each value.
350
     * The keys will be identical in both maps.
351
     *
352
     * @param callable $callback Accepts two arguments: key and value, should
353
     *                           return what the updated value will be.
354
     *
355
     * @return Map
356
     */
357
    public function map(callable $callback): Map
358
    {
359
        $mapped = new self();
360
361
        foreach ($this->pairs as $pair) {
362
            $mapped[$pair->key] = $callback($pair->key, $pair->value);
363
        }
364
365
        return $mapped;
366
    }
367
368
    /**
369
     * Returns a sequence of pairs representing all associations.
370
     *
371
     * @return Sequence
372
     */
373 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...
374
    {
375
        $sequence = new Vector();
376
377
        foreach ($this->pairs as $pair) {
378
            $sequence[] = $pair->copy();
379
        }
380
381
        return $sequence;
382
    }
383
384
    /**
385
     * Associates a key with a value, replacing a previous association if there
386
     * was one.
387
     *
388
     * @param mixed $key
389
     * @param mixed $value
390
     */
391
    public function put($key, $value)
392
    {
393
        $pair = $this->lookup($key);
394
395
        if ($pair) {
396
            $pair->value = $value;
397
            return;
398
        }
399
400
        $this->adjustCapacity();
401
        $this->pairs[] = new Pair($key, $value);
402
    }
403
404
    /**
405
     * Creates associations for all keys and corresponding values of either an
406
     * array or iterable object.
407
     *
408
     * @param array|\Traversable $values
409
     */
410
    public function putAll($values)
411
    {
412
        foreach ($values as $key => $value) {
413
            $this->put($key, $value);
414
        }
415
    }
416
417
    /**
418
     * Iteratively reduces the map to a single value using a callback.
419
     *
420
     * @param callable $callback Accepts the carry, key, and value, and
421
     *                           returns an updated carry value.
422
     *
423
     * @param mixed|null $initial Optional initial carry value.
424
     *
425
     * @return mixed The carry value of the final iteration, or the initial
426
     *               value if the map was empty.
427
     */
428
    public function reduce(callable $callback, $initial = null)
429
    {
430
        $carry = $initial;
431
432
        foreach ($this->pairs as $pair) {
433
            $carry = $callback($carry, $pair->key, $pair->value);
434
        }
435
436
        return $carry;
437
    }
438
439
    /**
440
     * Removes a key's association from the map and returns the associated value
441
     * or a provided default if provided.
442
     *
443
     * @param mixed $key
444
     * @param mixed $default
445
     *
446
     * @return mixed The associated value or fallback default if provided.
447
     *
448
     * @throws \OutOfBoundsException if no default was provided and the key is
449
     *                               not associated with a value.
450
     */
451
    public function remove($key, $default = null)
452
    {
453
        foreach ($this->pairs as $position => $pair) {
454
455
            // Check if the pair is the one we're looking for
456
            if ($this->identical($pair->key, $key)) {
457
458
                // Delete pair, return its value
459
                $value = $pair->value;
460
461
                array_splice($this->pairs, $position, 1, null);
462
                $this->adjustCapacity();
463
464
                return $value;
465
            }
466
        }
467
468
        // Check if a default was provided
469
        if (func_num_args() === 1) {
470
            throw new \OutOfBoundsException();
471
        }
472
473
        return $default;
474
    }
475
476
    /**
477
     * Returns a reversed copy of the map.
478
     */
479
    public function reverse(): Map
480
    {
481
        $reversed = new self();
482
483
        foreach (array_reverse($this->pairs) as $pair) {
484
            $reversed[$pair->key] = $pair->value;
485
        }
486
487
        return $reversed;
488
    }
489
490
    /**
491
     * Returns a sub-sequence of a given length starting at a specified offset.
492
     *
493
     * @param int $offset      If the offset is non-negative, the map will
494
     *                         start at that offset in the map. If offset is
495
     *                         negative, the map will start that far from the
496
     *                         end.
497
     *
498
     * @param int|null $length If a length is given and is positive, the
499
     *                         resulting set will have up to that many pairs in
500
     *                         it. If the requested length results in an
501
     *                         overflow, only pairs up to the end of the map
502
     *                         will be included.
503
     *
504
     *                         If a length is given and is negative, the map
505
     *                         will stop that many pairs from the end.
506
     *
507
     *                        If a length is not provided, the resulting map
508
     *                        will contains all pairs between the offset and
509
     *                        the end of the map.
510
     *
511
     * @return Map
512
     */
513
    public function slice(int $offset, int $length = null): Map
514
    {
515
        $map = new Map();
516
517
        if (func_num_args() === 1) {
518
            $slice = array_slice($this->pairs, $offset);
519
        } else {
520
            $slice = array_slice($this->pairs, $offset, $length);
521
        }
522
523
        foreach ($slice as $pair) {
524
            $map->put($pair->key, $pair->value);
525
        }
526
527
        return $map;
528
    }
529
530
    /**
531
     * Returns a sorted copy of the map, based on an optional callable
532
     * comparator. The map will be sorted by key if a comparator is not given.
533
     *
534
     * @param callable|null $comparator Accepts two values to be compared.
535
     *                                  Should return the result of a <=> b.
536
     *
537
     * @return Map
538
     */
539
    public function sort(callable $comparator = null): Map
540
    {
541
        $copy = $this->copy();
542
543
        if ($comparator) {
544
            usort($copy->pairs, $comparator);
545
        } else {
546
            usort($copy->pairs, function($a, $b) {
547
                return $a->key <=> $b->key;
548
            });
549
        }
550
551
        return $copy;
552
    }
553
554
    /**
555
     * @inheritDoc
556
     */
557
    public function toArray(): array
558
    {
559
        $array = [];
560
561
        foreach ($this->pairs as $pair) {
562
            $array[$pair->key] = $pair->value;
563
        }
564
565
        return $array;
566
    }
567
568
    /**
569
     * Returns a sequence of all the associated values in the Map.
570
     *
571
     * @return Sequence
572
     */
573 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...
574
    {
575
        $sequence = new Vector();
576
577
        foreach ($this->pairs as $pair) {
578
            $sequence[] = $pair->value;
579
        }
580
581
        return $sequence;
582
    }
583
584
    /**
585
     * Get iterator
586
     */
587
    public function getIterator()
588
    {
589
        foreach ($this->pairs as $pair) {
590
            yield $pair->key => $pair->value;
591
        }
592
    }
593
594
    /**
595
     * Debug Info
596
     */
597
    public function __debugInfo()
598
    {
599
        $debug = [];
600
601
        foreach ($this->pairs as $pair) {
602
            $debug[] = [$pair->key, $pair->value];
603
        }
604
605
        return $debug;
606
    }
607
608
    /**
609
     * @inheritdoc
610
     */
611
    public function offsetSet($offset, $value)
612
    {
613
        $this->put($offset, $value);
614
    }
615
616
    /**
617
     * @inheritdoc
618
     *
619
     * @throws OutOfBoundsException
620
     */
621
    public function &offsetGet($offset)
622
    {
623
        $pair = $this->lookup($offset);
624
625
        if ($pair) {
626
            return $pair->value;
627
        }
628
629
        throw new OutOfBoundsException();
630
    }
631
632
    /**
633
     * @inheritdoc
634
     */
635
    public function offsetUnset($offset)
636
    {
637
        $this->remove($offset, null);
638
    }
639
640
    /**
641
     * @inheritdoc
642
     */
643
    public function offsetExists($offset)
644
    {
645
        return $this->get($offset, null) !== null;
646
    }
647
}
648