Passed
Push — master ( d0ebb7...0d8755 )
by Rudi
03:39
created

Map::hasValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 0
cts 4
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
namespace Ds;
3
4
use OutOfBoundsException;
5
use OutOfRangeException;
6
use Traversable;
7
use UnderflowException;
8
9
/**
10
 * A Map is a sequential collection of key-value pairs, almost identical to an
11
 * array used in a similar context. Keys can be any type, but must be unique.
12
 *
13
 * @package Ds
14
 */
15
final class Map implements Collection, \ArrayAccess
16
{
17
    use Traits\GenericCollection;
18
    use Traits\SquaredCapacity;
19
20
    const MIN_CAPACITY = 8;
21
22
    /**
23
     * @var array internal array to store pairs
24
     */
25
    private $pairs = [];
26
27
    /**
28
     * Creates a new instance.
29
     *
30
     * @param array|\Traversable|null $values
31
     */
32
    public function __construct($values = null)
33
    {
34
        if (func_num_args()) {
35
            $this->putAll($values);
36
        }
37
    }
38
39
    /**
40
     * Updates all values by applying a callback function to each value.
41
     *
42
     * @param callable $callback Accepts two arguments: key and value, should
43
     *                           return what the updated value will be.
44
     */
45
    public function apply(callable $callback)
46
    {
47
        foreach ($this->pairs as &$pair) {
48
            $pair->value = $callback($pair->key, $pair->value);
49
        }
50
    }
51
52
    /**
53
     * @inheritDoc
54
     */
55
    public function clear()
56
    {
57
        $this->pairs = [];
58
        $this->capacity = self::MIN_CAPACITY;
59
    }
60
61
    /**
62
     * Return the first Pair from the Map
63
     *
64
     * @return Pair
65
     *
66
     * @throws UnderflowException
67
     */
68
    public function first(): Pair
69
    {
70
        if ($this->isEmpty()) {
71
            throw new UnderflowException();
72
        }
73
74
        return $this->pairs[0];
75
    }
76
77
    /**
78
     * Return the last Pair from the Map
79
     *
80
     * @return Pair
81
     *
82
     * @throws UnderflowException
83
     */
84
    public function last(): Pair
85
    {
86
        if ($this->isEmpty()) {
87
            throw new UnderflowException();
88
        }
89
90
        return $this->pairs[count($this->pairs) - 1];
91
    }
92
93
    /**
94
     * Return the pair at a specified position in the Map
95
     *
96
     * @param int $position
97
     *
98
     * @return Pair
99
     *
100
     * @throws OutOfRangeException
101
     */
102
    public function skip(int $position): Pair
103
    {
104
        if ($position < 0 || $position >= count($this->pairs)) {
105
            throw new OutOfRangeException();
106
        }
107
108
        return $this->pairs[$position]->copy();
109
    }
110
111
    /**
112
     * Returns the result of associating all keys of a given traversable object
113
     * or array with their corresponding values, as well as those of this 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
        return $merged;
124
    }
125
126
    /**
127
     * Creates a new map containing the pairs of the current instance whose keys
128
     * are also present in the given map. In other words, returns a copy of the
129
     * current map with all keys removed that are not also in the other map.
130
     *
131
     * @param Map $map The other map.
132
     *
133
     * @return Map A new map containing the pairs of the current instance
134
     *                 whose keys are also present in the given map. In other
135
     *                 words, returns a copy of the current map with all keys
136
     *                 removed that are not also in the other map.
137
     */
138
    public function intersect(Map $map): Map
139
    {
140
        return $this->filter(function($key) use ($map) {
141
            return $map->hasKey($key);
142
        });
143
    }
144
145
    /**
146
     * Returns the result of removing all keys from the current instance that
147
     * are present in a given map.
148
     *
149
     * @param Map $map The map containing the keys to exclude.
150
     *
151
     * @return Map The result of removing all keys from the current instance
152
     *                 that are present in a given map.
153
     */
154
    public function diff(Map $map): Map
155
    {
156
        return $this->filter(function($key) use ($map) {
157
            return ! $map->hasKey($key);
158
        });
159
    }
160
161
    /**
162
     * Determines whether two keys are equal.
163
     *
164
     * @param mixed $a
165
     * @param mixed $b
166
     *
167
     * @return bool
168
     */
169
    private function keysAreEqual($a, $b): bool
170
    {
171
        if (is_object($a) && $a instanceof Hashable) {
172
            return get_class($a) === get_class($b) && $a->equals($b);
173
        }
174
175
        return $a === $b;
176
    }
177
178
    /**
179
     * Attempts to look up a key in the table.
180
     *
181
     * @param $key
182
     *
183
     * @return Pair|null
184
     */
185
    private function lookupKey($key)
186
    {
187
        foreach ($this->pairs as $pair) {
188
            if ($this->keysAreEqual($pair->key, $key)) {
189
                return $pair;
190
            }
191
        }
192
    }
193
194
    /**
195
     * Attempts to look up a value in the table.
196
     *
197
     * @param $value
198
     *
199
     * @return Pair|null
200
     */
201
    private function lookupValue($value)
202
    {
203
        foreach ($this->pairs as $pair) {
204
            if ($pair->value === $value) {
205
                return $pair;
206
            }
207
        }
208
    }
209
210
    /**
211
     * Returns whether an association a given key exists.
212
     *
213
     * @param mixed $key
214
     *
215
     * @return bool
216
     */
217
    public function hasKey($key): bool
218
    {
219
        return $this->lookupKey($key) !== null;
220
    }
221
222
    /**
223
     * Returns whether an association for a given value exists.
224
     *
225
     * @param mixed $value
226
     *
227
     * @return bool
228
     */
229
    public function hasValue($value): bool
230
    {
231
        return $this->lookupValue($value) !== null;
232
    }
233
234
    /**
235
     * @inheritDoc
236
     */
237
    public function count(): int
238
    {
239
        return count($this->pairs);
240
    }
241
242
    /**
243
     * Returns a new map containing only the values for which a predicate
244
     * returns true. A boolean test will be used if a predicate is not provided.
245
     *
246
     * @param callable|null $callback Accepts a key and a value, and returns:
247
     *                                true : include the value,
248
     *                                false: skip the value.
249
     *
250
     * @return Map
251
     */
252
    public function filter(callable $callback = null): Map
253
    {
254
        $filtered = new self();
255
256
        foreach ($this as $key => $value) {
257
            if ($callback ? $callback($key, $value) : $value) {
258
                $filtered->put($key, $value);
259
            }
260
        }
261
262
        return $filtered;
263
    }
264
265
    /**
266
     * Returns the value associated with a key, or an optional default if the
267
     * key is not associated with a value.
268
     *
269
     * @param mixed $key
270
     * @param mixed $default
271
     *
272
     * @return mixed The associated value or fallback default if provided.
273
     *
274
     * @throws OutOfBoundsException if no default was provided and the key is
275
     *                               not associated with a value.
276
     */
277
    public function get($key, $default = null)
278
    {
279
        if (($pair = $this->lookupKey($key))) {
280
            return $pair->value;
281
        }
282
283
        // Check if a default was provided.
284
        if (func_num_args() === 1) {
285
            throw new OutOfBoundsException();
286
        }
287
288
        return $default;
289
    }
290
291
    /**
292
     * Returns a set of all the keys in the map.
293
     *
294
     * @return Set
295
     */
296
    public function keys(): Set
297
    {
298
        $key = function($pair) {
299
            return $pair->key;
300
        };
301
302
        return new Set(array_map($key, $this->pairs));
303
    }
304
305
    /**
306
     * Returns a new map using the results of applying a callback to each value.
307
     *
308
     * The keys will be equal in both maps.
309
     *
310
     * @param callable $callback Accepts two arguments: key and value, should
311
     *                           return what the updated value will be.
312
     *
313
     * @return Map
314
     */
315
    public function map(callable $callback): Map
316
    {
317
        $mapped = new self();
318
        foreach ($this->pairs as $pair) {
319
            $mapped->put($pair->key, $callback($pair->key, $pair->value));
320
        }
321
322
        return $mapped;
323
    }
324
325
    /**
326
     * Returns a sequence of pairs representing all associations.
327
     *
328
     * @return Sequence
329
     */
330
    public function pairs(): Sequence
331
    {
332
        $copy = function($pair) {
333
            return $pair->copy();
334
        };
335
336
        return new Vector(array_map($copy, $this->pairs));
337
    }
338
339
    /**
340
     * Associates a key with a value, replacing a previous association if there
341
     * was one.
342
     *
343
     * @param mixed $key
344
     * @param mixed $value
345
     */
346
    public function put($key, $value)
347
    {
348
        $pair = $this->lookupKey($key);
349
350
        if ($pair) {
351
            $pair->value = $value;
352
353
        } else {
354
            $this->checkCapacity();
355
            $this->pairs[] = new Pair($key, $value);
356
        }
357
    }
358
359
    /**
360
     * Creates associations for all keys and corresponding values of either an
361
     * array or iterable object.
362
     *
363
     * @param \Traversable|array $values
364
     */
365
    public function putAll($values)
366
    {
367
        foreach ($values as $key => $value) {
368
            $this->put($key, $value);
369
        }
370
    }
371
372
    /**
373
     * Iteratively reduces the map to a single value using a callback.
374
     *
375
     * @param callable $callback Accepts the carry, key, and value, and
376
     *                           returns an updated carry value.
377
     *
378
     * @param mixed|null $initial Optional initial carry value.
379
     *
380
     * @return mixed The carry value of the final iteration, or the initial
381
     *               value if the map was empty.
382
     */
383
    public function reduce(callable $callback, $initial = null)
384
    {
385
        $carry = $initial;
386
387
        foreach ($this->pairs as $pair) {
388
            $carry = $callback($carry, $pair->key, $pair->value);
389
        }
390
391
        return $carry;
392
    }
393
394
    /**
395
     * Completely removes a pair from the internal array by position. It is
396
     * important to remove it from the array and not just use 'unset'.
397
     */
398
    private function delete(int $position)
399
    {
400
        $pair  = $this->pairs[$position];
401
        $value = $pair->value;
402
403
        array_splice($this->pairs, $position, 1, null);
404
        $this->checkCapacity();
405
406
        return $value;
407
    }
408
409
    /**
410
     * Removes a key's association from the map and returns the associated value
411
     * or a provided default if provided.
412
     *
413
     * @param mixed $key
414
     * @param mixed $default
415
     *
416
     * @return mixed The associated value or fallback default if provided.
417
     *
418
     * @throws \OutOfBoundsException if no default was provided and the key is
419
     *                               not associated with a value.
420
     */
421
    public function remove($key, $default = null)
422
    {
423
        foreach ($this->pairs as $position => $pair) {
424
            if ($this->keysAreEqual($pair->key, $key)) {
425
                return $this->delete($position);
426
            }
427
        }
428
429
        // Check if a default was provided
430
        if (func_num_args() === 1) {
431
            throw new \OutOfBoundsException();
432
        }
433
434
        return $default;
435
    }
436
437
    /**
438
     * Returns a reversed copy of the map.
439
     *
440
     * @return Map
0 ignored issues
show
Documentation introduced by
Should the return type not be Map|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
441
     */
442
    public function reverse()
443
    {
444
        $this->pairs = array_reverse($this->pairs);
445
    }
446
447
    /**
448
     * Returns a reversed copy of the map.
449
     *
450
     * @return Map
451
     */
452
    public function reversed(): Map
453
    {
454
        $reversed = new self();
455
        $reversed->pairs = array_reverse($this->pairs);
456
457
        return $reversed;
458
    }
459
460
    /**
461
     * Returns a sub-sequence of a given length starting at a specified offset.
462
     *
463
     * @param int $offset      If the offset is non-negative, the map will
464
     *                         start at that offset in the map. If offset is
465
     *                         negative, the map will start that far from the
466
     *                         end.
467
     *
468
     * @param int|null $length If a length is given and is positive, the
469
     *                         resulting set will have up to that many pairs in
470
     *                         it. If the requested length results in an
471
     *                         overflow, only pairs up to the end of the map
472
     *                         will be included.
473
     *
474
     *                         If a length is given and is negative, the map
475
     *                         will stop that many pairs from the end.
476
     *
477
     *                        If a length is not provided, the resulting map
478
     *                        will contains all pairs between the offset and
479
     *                        the end of the map.
480
     *
481
     * @return Map
482
     */
483
    public function slice(int $offset, int $length = null): Map
484
    {
485
        $map = new self();
486
487
        if (func_num_args() === 1) {
488
            $slice = array_slice($this->pairs, $offset);
489
        } else {
490
            $slice = array_slice($this->pairs, $offset, $length);
491
        }
492
493
        foreach ($slice as $pair) {
494
            $map->put($pair->key, $pair->value);
495
        }
496
497
        return $map;
498
    }
499
500
    /**
501
     * Sorts the map in-place, based on an optional callable comparator.
502
     *
503
     * The map will be sorted by value.
504
     *
505
     * @param callable|null $comparator Accepts two values to be compared.
506
     */
507 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...
508
    {
509
        if ($comparator) {
510
            usort($this->pairs, function($a, $b) use ($comparator) {
511
                return $comparator($a->value, $b->value);
512
            });
513
514
        } else {
515
            usort($this->pairs, function($a, $b) {
516
                return $a->value <=> $b->value;
517
            });
518
        }
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
    public function sorted(callable $comparator = null): Map
530
    {
531
        $copy = $this->copy();
532
        $copy->sort($comparator);
533
        return $copy;
534
    }
535
536
    /**
537
     * Sorts the map in-place, based on an optional callable comparator.
538
     *
539
     * The map will be sorted by key.
540
     *
541
     * @param callable|null $comparator Accepts two keys to be compared.
542
     */
543 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...
544
    {
545
        if ($comparator) {
546
            usort($this->pairs, function($a, $b) use ($comparator) {
547
                return $comparator($a->key, $b->key);
548
            });
549
550
        } else {
551
            usort($this->pairs, function($a, $b) {
552
                return $a->key <=> $b->key;
553
            });
554
        }
555
    }
556
557
    /**
558
     * Returns a sorted copy of the map, based on an optional callable
559
     * comparator. The map will be sorted by key.
560
     *
561
     * @param callable|null $comparator Accepts two keys to be compared.
562
     *
563
     * @return Map
564
     */
565
    public function ksorted(callable $comparator = null): Map
566
    {
567
        $copy = $this->copy();
568
        $copy->ksort($comparator);
569
        return $copy;
570
    }
571
572
    /**
573
     * Returns the sum of all values in the map.
574
     *
575
     * @return int|float The sum of all the values in the map.
576
     */
577
    public function sum()
578
    {
579
        return $this->values()->sum();
580
    }
581
582
    /**
583
     * @inheritDoc
584
     */
585
    public function toArray(): array
586
    {
587
        $array = [];
588
589
        foreach ($this->pairs as $pair) {
590
            $array[$pair->key] = $pair->value;
591
        }
592
593
        return $array;
594
    }
595
596
    /**
597
     * Returns a sequence of all the associated values in the Map.
598
     *
599
     * @return Sequence
600
     */
601
    public function values(): Sequence
602
    {
603
        $value = function($pair) {
604
            return $pair->value;
605
        };
606
607
        return new Vector(array_map($value, $this->pairs));
608
    }
609
610
    /**
611
     * Creates a new map that contains the pairs of the current instance as well
612
     * as the pairs of another map.
613
     *
614
     * @param Map $map The other map, to combine with the current instance.
615
     *
616
     * @return Map A new map containing all the pairs of the current
617
     *                 instance as well as another map.
618
     */
619
    public function union(Map $map): Map
620
    {
621
        return $this->merge($map);
622
    }
623
624
    /**
625
     * Creates a new map using keys of either the current instance or of another
626
     * map, but not of both.
627
     *
628
     * @param Map $map
629
     *
630
     * @return Map A new map containing keys in the current instance as well
631
     *                 as another map, but not in both.
632
     */
633
    public function xor(Map $map): Map
634
    {
635
        return $this->merge($map)->filter(function($key) use ($map) {
636
            return $this->hasKey($key) ^ $map->hasKey($key);
637
        });
638
    }
639
640
    /**
641
     * @inheritDoc
642
     */
643
    public function getIterator()
644
    {
645
        foreach ($this->pairs as $pair) {
646
            yield $pair->key => $pair->value;
647
        }
648
    }
649
650
    /**
651
     * Returns a representation to be used for var_dump and print_r.
652
     */
653
    public function __debugInfo()
654
    {
655
        return $this->pairs()->toArray();
656
    }
657
658
    /**
659
     * @inheritdoc
660
     */
661
    public function offsetSet($offset, $value)
662
    {
663
        $this->put($offset, $value);
664
    }
665
666
    /**
667
     * @inheritdoc
668
     *
669
     * @throws OutOfBoundsException
670
     */
671
    public function &offsetGet($offset)
672
    {
673
        $pair = $this->lookupKey($offset);
674
675
        if ($pair) {
676
            return $pair->value;
677
        }
678
679
        throw new OutOfBoundsException();
680
    }
681
682
    /**
683
     * @inheritdoc
684
     */
685
    public function offsetUnset($offset)
686
    {
687
        $this->remove($offset, null);
688
    }
689
690
    /**
691
     * @inheritdoc
692
     */
693
    public function offsetExists($offset)
694
    {
695
        return $this->get($offset, null) !== null;
696
    }
697
698
    /**
699
     * Returns a representation that can be natively converted to JSON, which is
700
     * called when invoking json_encode.
701
     *
702
     * @return mixed
703
     *
704
     * @see \JsonSerializable
705
     */
706
    public function jsonSerialize()
707
    {
708
        return (object) $this->toArray();
709
    }
710
}
711