Completed
Push — master ( 2c7298...7aefdd )
by Rudi
02:17
created

Map::getIterator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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