Completed
Push — master ( 0e7c11...f3685f )
by Rudi
03:29
created

Map::ksort()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 17
Ratio 100 %

Importance

Changes 0
Metric Value
dl 17
loc 17
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 9
nc 2
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 $this->pairs[$position]->copy();
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 = $this->copy();
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 copy()
272
    {
273
        return new self($this);
274
    }
275
276
    /**
277
     * @inheritDoc
278
     */
279
    public function count(): int
280
    {
281
        return count($this->pairs);
282
    }
283
284
    /**
285
     * Returns a new map containing only the values for which a predicate
286
     * returns true. A boolean test will be used if a predicate is not provided.
287
     *
288
     * @param callable|null $predicate Accepts a key and a value, and returns:
289
     *                                 true : include the value,
290
     *                                 false: skip the value.
291
     *
292
     * @return Map
293
     */
294 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...
295
    {
296
        $filtered = new self();
297
298
        foreach ($this as $key => $value) {
299
            if ($predicate ? $predicate($key, $value) : $value) {
300
                $filtered->put($key, $value);
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->lookupKey($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 keysAreEqual 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
    public function pairs(): Sequence
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->lookupKey($key);
394
395
        if ($pair) {
396
            $pair->value = $value;
397
398
        } else {
399
            $this->adjustCapacity();
400
            $this->pairs[] = new Pair($key, $value);
401
        }
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
    private function delete(int $position)
440
    {
441
        $pair  = $this->pairs[$position];
442
        $value = $pair->value;
443
444
        array_splice($this->pairs, $position, 1, null);
445
446
        $this->adjustCapacity();
447
        return $value;
448
    }
449
450
    /**
451
     * Removes a key's association from the map and returns the associated value
452
     * or a provided default if provided.
453
     *
454
     * @param mixed $key
455
     * @param mixed $default
456
     *
457
     * @return mixed The associated value or fallback default if provided.
458
     *
459
     * @throws \OutOfBoundsException if no default was provided and the key is
460
     *                               not associated with a value.
461
     */
462
    public function remove($key, $default = null)
463
    {
464
        foreach ($this->pairs as $position => $pair) {
465
            if ($this->keysAreEqual($pair->key, $key)) {
466
                return $this->delete($position);
467
            }
468
        }
469
470
        // Check if a default was provided
471
        if (func_num_args() === 1) {
472
            throw new \OutOfBoundsException();
473
        }
474
475
        return $default;
476
    }
477
478
    /**
479
     * Returns a reversed copy of the map.
480
     */
481
    public function reverse(): Map
482
    {
483
        $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...
484
        $reversed->pairs = array_reverse($this->pairs);
485
486
        return $reversed;
487
    }
488
489
    /**
490
     * Returns a sub-sequence of a given length starting at a specified offset.
491
     *
492
     * @param int $offset      If the offset is non-negative, the map will
493
     *                         start at that offset in the map. If offset is
494
     *                         negative, the map will start that far from the
495
     *                         end.
496
     *
497
     * @param int|null $length If a length is given and is positive, the
498
     *                         resulting set will have up to that many pairs in
499
     *                         it. If the requested length results in an
500
     *                         overflow, only pairs up to the end of the map
501
     *                         will be included.
502
     *
503
     *                         If a length is given and is negative, the map
504
     *                         will stop that many pairs from the end.
505
     *
506
     *                        If a length is not provided, the resulting map
507
     *                        will contains all pairs between the offset and
508
     *                        the end of the map.
509
     *
510
     * @return Map
511
     */
512
    public function slice(int $offset, int $length = null): Map
513
    {
514
        $map = new Map();
515
516
        if (func_num_args() === 1) {
517
            $slice = array_slice($this->pairs, $offset);
518
        } else {
519
            $slice = array_slice($this->pairs, $offset, $length);
520
        }
521
522
        foreach ($slice as $pair) {
523
            $map->put($pair->key, $pair->value);
524
        }
525
526
        return $map;
527
    }
528
529
    /**
530
     * Returns a sorted copy of the map, based on an optional callable
531
     * comparator. The map will be sorted by value.
532
     *
533
     * @param callable|null $comparator Accepts two values to be compared.
534
     *
535
     * @return Map
536
     */
537 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...
538
    {
539
        $sorted = $this->copy();
540
541
        if ($comparator) {
542
            usort($sorted->pairs, function($a, $b) use ($comparator) {
543
                return $comparator($a->value, $b->value);
544
            });
545
546
        } else {
547
            usort($sorted->pairs, function($a, $b) {
548
                return $a->value <=> $b->value;
549
            });
550
        }
551
552
        return $sorted;
553
    }
554
555
    /**
556
     * Returns a sorted copy of the map, based on an optional callable
557
     * comparator. The map will be sorted by key.
558
     *
559
     * @param callable|null $comparator Accepts two keys to be compared.
560
     *
561
     * @return Map
562
     */
563 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...
564
    {
565
        $sorted = $this->copy();
566
567
        if ($comparator) {
568
            usort($sorted->pairs, function($a, $b) use ($comparator) {
569
                return $comparator($a->key, $b->key);
570
            });
571
572
        } else {
573
            usort($sorted->pairs, function($a, $b) {
574
                return $a->key <=> $b->key;
575
            });
576
        }
577
578
        return $sorted;
579
    }
580
581
    /**
582
     * @inheritDoc
583
     */
584
    public function toArray(): array
585
    {
586
        $array = [];
587
588
        foreach ($this->pairs as $pair) {
589
            $array[$pair->key] = $pair->value;
590
        }
591
592
        return $array;
593
    }
594
595
    /**
596
     * Returns a sequence of all the associated values in the Map.
597
     *
598
     * @return Sequence
599
     */
600
    public function values(): Sequence
601
    {
602
        $sequence = new Vector();
603
604
        foreach ($this->pairs as $pair) {
605
            $sequence->push($pair->value);
606
        }
607
608
        return $sequence;
609
    }
610
611
    /**
612
     * Get iterator
613
     */
614
    public function getIterator()
615
    {
616
        foreach ($this->pairs as $pair) {
617
            yield $pair->key => $pair->value;
618
        }
619
    }
620
621
    /**
622
     * Debug Info
623
     */
624
    public function __debugInfo()
625
    {
626
        return $this->pairs()->toArray();
627
    }
628
629
    /**
630
     * @inheritdoc
631
     */
632
    public function offsetSet($offset, $value)
633
    {
634
        $this->put($offset, $value);
635
    }
636
637
    /**
638
     * @inheritdoc
639
     *
640
     * @throws OutOfBoundsException
641
     */
642
    public function &offsetGet($offset)
643
    {
644
        $pair = $this->lookupKey($offset);
645
646
        if ($pair) {
647
            return $pair->value;
648
        }
649
650
        throw new OutOfBoundsException();
651
    }
652
653
    /**
654
     * @inheritdoc
655
     */
656
    public function offsetUnset($offset)
657
    {
658
        $this->remove($offset, null);
659
    }
660
661
    /**
662
     * @inheritdoc
663
     */
664
    public function offsetExists($offset)
665
    {
666
        return $this->get($offset, null) !== null;
667
    }
668
}
669