• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

azjezz / psl / 9977367388

17 Jul 2024 03:47PM UTC coverage: 97.158% (-0.1%) from 97.271%
9977367388

push

github

web-flow
feat(collections): introduce `Set`, `SetInterface`, `MutableSet`, and `MutableSetInterface` (#482)

Signed-off-by: azjezz <azjezz@protonmail.com>

207 of 217 new or added lines in 8 files covered. (95.39%)

2 existing lines in 1 file now uncovered.

5230 of 5383 relevant lines covered (97.16%)

50.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.94
/src/Psl/Collection/MutableSet.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\Collection;
6

7
use Closure;
8
use Psl\Dict;
9
use Psl\Iter;
10
use Psl\Vec;
11

12
use function array_key_exists;
13
use function array_key_first;
14
use function array_key_last;
15
use function count;
16

17
/**
18
 * @template T of array-key
19
 *
20
 * @implements MutableSetInterface<T>
21
 */
22
final class MutableSet implements MutableSetInterface
23
{
24
    /**
25
     * @var array<T, T>
26
     */
27
    private array $elements = [];
28

29
    /**
30
     * Creates a new `MutableSet` containing the values of the given array.
31
     *
32
     * @param array<array-key, T> $elements
33
     *
34
     * @psalm-mutation-free
35
     */
36
    public function __construct(array $elements)
37
    {
38
        $set = [];
58✔
39
        foreach ($elements as $element) {
58✔
40
            $set[$element] = $element;
58✔
41
        }
42

43
        $this->elements = $set;
58✔
44
    }
45

46
    /**
47
     * Creates and returns a default instance of {@see MutableSet}.
48
     *
49
     * @return static A default instance of {@see MutableSet}.
50
     *
51
     * @psalm-external-mutation-free
52
     */
53
    public static function default(): static
54
    {
55
        return new self([]);
6✔
56
    }
57

58
    /**
59
     * Create a set from the given array, using the values of the array as the set values.
60
     *
61
     * @template Ts of array-key
62
     *
63
     * @param array<array-key, Ts> $elements
64
     *
65
     * @return MutableSet<Ts>
66
     *
67
     * @pure
68
     */
69
    public static function fromArray(array $elements): MutableSet
70
    {
71
        return new self($elements);
34✔
72
    }
73

74
    /**
75
     * Create a set from the given $elements array, using the keys of the array as the set values.
76
     *
77
     * @template Ts of array-key
78
     *
79
     * @param array<Ts, mixed> $elements
80
     *
81
     * @return MutableSet<Ts>
82
     *
83
     * @pure
84
     */
85
    public static function fromArrayKeys(array $elements): MutableSet
86
    {
87
        /** @var array<Ts, Ts> $set */
NEW
88
        $set = [];
×
NEW
89
        foreach ($elements as $element => $_) {
×
NEW
90
            $set[$element] = $element;
×
91
        }
92

NEW
93
        return new self($set);
×
94
    }
95

96
    /**
97
     * Returns the first value in the current `MutableSet`.
98
     *
99
     * @return T|null The first value in the current `MutableSet`, or `null` if the
100
     *                current `MutableSet` is empty.
101
     *
102
     * @psalm-mutation-free
103
     */
104
    public function first(): null|int|string
105
    {
106
        return array_key_first($this->elements);
2✔
107
    }
108

109
    /**
110
     * Returns the last value in the current `MutableSet`.
111
     *
112
     * @return T|null The last value in the current `MutableSet`, or `null` if the
113
     *                current `MutableSet` is empty.
114
     *
115
     * @psalm-mutation-free
116
     */
117
    public function last(): null|int|string
118
    {
119
        return array_key_last($this->elements);
2✔
120
    }
121

122
    /**
123
     * Retrieve an external iterator.
124
     *
125
     * @return Iter\Iterator<T, T>
126
     */
127
    public function getIterator(): Iter\Iterator
128
    {
129
        return Iter\Iterator::create($this->elements);
31✔
130
    }
131

132
    /**
133
     * Is the set empty?
134
     *
135
     * @psalm-mutation-free
136
     */
137
    public function isEmpty(): bool
138
    {
139
        return [] === $this->elements;
1✔
140
    }
141

142
    /**
143
     * Get the number of elements in the current `MutableSet`.
144
     *
145
     * @psalm-mutation-free
146
     *
147
     * @return int<0, max>
148
     */
149
    public function count(): int
150
    {
151
        /** @var int<0, max> */
152
        return count($this->elements);
15✔
153
    }
154

155
    /**
156
     * Get an array copy of the current `MutableSet`.
157
     *
158
     * @return array<T, T>
159
     *
160
     * @psalm-mutation-free
161
     */
162
    public function toArray(): array
163
    {
164
        return $this->elements;
25✔
165
    }
166

167
    /**
168
     * Get an array copy of the current `MutableSet`.
169
     *
170
     * @return array<T, T>
171
     *
172
     * @psalm-mutation-free
173
     */
174
    public function jsonSerialize(): array
175
    {
176
        return $this->elements;
1✔
177
    }
178

179
    /**
180
     * Returns the provided value if it exists in the current `MutableSet`.
181
     *
182
     * As {@see MutableSet} does not have keys, this method checks if the value exists in the set.
183
     * If the value exists, it is returned to indicate presence in the set. If the value does not exist,
184
     * an {@see Exception\OutOfBoundsException} is thrown to indicate the absence of the value.
185
     *
186
     * @param T $k
187
     *
188
     * @throws Exception\OutOfBoundsException If $k is out-of-bounds.
189
     *
190
     * @return T
191
     *
192
     * @psalm-mutation-free
193
     */
194
    public function at(int|string $k): int|string
195
    {
196
        if (!array_key_exists($k, $this->elements)) {
9✔
197
            throw Exception\OutOfBoundsException::for($k);
1✔
198
        }
199

200
        // the key exists, and we know it's the same as the value.
201
        return $k;
9✔
202
    }
203

204
    /**
205
     * Determines if the specified value is in the current set.
206
     *
207
     * As {@see MutableSet} does not have keys, this method checks if the value exists in the set.
208
     * If the value exists, it returns true to indicate presence in the set. If the value does not exist,
209
     * it returns false to indicate the absence of the value.
210
     *
211
     * @param T $k
212
     *
213
     * @return bool True if the value is in the set, false otherwise.
214
     *
215
     * @psalm-mutation-free
216
     */
217
    public function contains(int|string $k): bool
218
    {
219
        return array_key_exists($k, $this->elements);
2✔
220
    }
221

222
    /**
223
     * Returns the provided value if it is part of the set, or null if it is not.
224
     *
225
     * As {@see MutableSet} does not have keys, this method checks if the value exists in the set.
226
     * If the value exists, it is returned to indicate presence in the set. If the value does not exist,
227
     * null is returned to indicate the absence of the value.
228
     *
229
     * @param T $k
230
     *
231
     * @return T|null
232
     *
233
     * @psalm-mutation-free
234
     */
235
    public function get(int|string $k): null|int|string
236
    {
237
        return $this->elements[$k] ?? null;
1✔
238
    }
239

240
    /**
241
     * Returns the first key in the current `MutableSet`.
242
     *
243
     * As {@see MutableSet} does not have keys, this method acts as an alias for {@see MutableSet::first()}.
244
     *
245
     * @return T|null The first value in the current `MutableSet`, or `null` if the
246
     *                current `MutableSet` is empty.
247
     *
248
     * @psalm-mutation-free
249
     */
250
    public function firstKey(): null|int|string
251
    {
252
        return $this->first();
1✔
253
    }
254

255
    /**
256
     * Returns the last key in the current `MutableSet`.
257
     *
258
     * As {@see MutableSet} does not have keys, this method acts as an alias for {@see MutableSet::last()}.
259
     *
260
     * @return T|null The last value in the current `MutableSet`, or `null` if the
261
     *                current `MutableSet` is empty.
262
     *
263
     * @psalm-mutation-free
264
     */
265
    public function lastKey(): null|int|string
266
    {
267
        return $this->last();
1✔
268
    }
269

270
    /**
271
     * Returns the key of the first element that matches the search value.
272
     *
273
     * If no element matches the search value, this function returns null.
274
     *
275
     * As {@see MutableSet} does not have keys, this method returns the value itself.
276
     *
277
     * @param T $search_value The value that will be search for in the current
278
     *                        `MutableSet`.
279
     *
280
     * @return T|null The value if its found, null otherwise.
281
     *
282
     * @psalm-mutation-free
283
     */
284
    public function linearSearch(mixed $search_value): null|int|string
285
    {
286
        foreach ($this->elements as $element) {
1✔
287
            if ($search_value === $element) {
1✔
288
                return $element;
1✔
289
            }
290
        }
291

292
        return null;
1✔
293
    }
294

295
    /**
296
     * Removes the specified value from the current set.
297
     *
298
     * If the value is not in the current set, the current set is unchanged.
299
     *
300
     * @param T $k The value to remove.
301
     *
302
     * @return MutableSet<T> Returns itself.
303
     */
304
    public function remove(int|string $k): MutableSet
305
    {
306
        unset($this->elements[$k]);
2✔
307

308
        return $this;
2✔
309
    }
310

311
    /**
312
     * Removes all elements from the set.
313
     *
314
     * @return MutableSet<T> Returns itself
315
     *
316
     * @psalm-external-mutation-free
317
     */
318
    public function clear(): MutableSet
319
    {
320
        $this->elements = [];
1✔
321

322
        return $this;
1✔
323
    }
324

325
    /**
326
     * Add a value to the set and return the set itself.
327
     *
328
     * @param T $v The value to add.
329
     *
330
     * @return MutableSet<T> Returns itself.
331
     *
332
     * @psalm-external-mutation-free
333
     */
334
    public function add(mixed $v): MutableSet
335
    {
336
        $this->elements[$v] = $v;
3✔
337

338
        return $this;
3✔
339
    }
340

341
    /**
342
     * For every element in the provided elements iterable, add the value into the current set.
343
     *
344
     * @param iterable<T> $elements The elements with the new values to add
345
     *
346
     * @return MutableSet<T> returns itself.
347
     *
348
     * @psalm-external-mutation-free
349
     */
350
    public function addAll(iterable $elements): MutableSet
351
    {
352
        foreach ($elements as $item) {
1✔
353
            $this->add($item);
1✔
354
        }
355

356
        return $this;
1✔
357
    }
358

359
    /**
360
     * Returns a `MutableVector` containing the values of the current `MutableSet`.
361
     *
362
     * @return MutableVector<T>
363
     *
364
     * @psalm-mutation-free
365
     */
366
    public function values(): MutableVector
367
    {
368
        return MutableVector::fromArray($this->elements);
2✔
369
    }
370

371
    /**
372
     * As {@see MutableSet} does not have keys, this method acts as an alias for {@see MutableSet::values()}.
373
     *
374
     * @return MutableVector<T>
375
     *
376
     * @psalm-mutation-free
377
     */
378
    public function keys(): MutableVector
379
    {
380
        return MutableVector::fromArray($this->elements);
1✔
381
    }
382

383
    /**
384
     * Returns a `MutableSet` containing the values of the current `MutableSet`
385
     * that meet a supplied condition.
386
     *
387
     * Only values that meet a certain criteria are affected by a call to
388
     * `filter()`, while all values are affected by a call to `map()`.
389
     *
390
     * The keys associated with the current `MutableSet` remain unchanged in the
391
     * returned `MutableSet`.
392
     *
393
     * @param (Closure(T): bool) $fn The callback containing the condition to apply to the current
394
     *                               `MutableSet` values.
395
     *
396
     * @return MutableSet<T> A `MutableSet` containing the values after a user-specified condition
397
     *                       is applied.
398
     */
399
    public function filter(Closure $fn): MutableSet
400
    {
401
        return new MutableSet(Dict\filter_keys($this->elements, $fn));
2✔
402
    }
403

404
    /**
405
     * Applies a user-defined condition to each value in the `MutableSet`,
406
     *  considering the value as both key and value.
407
     *
408
     * This method extends {@see MutableSet::filter()} by providing the value twice to the
409
     *  callback function: once as the "key" and once as the "value", even though {@see MutableSet} do not have traditional key-value pairs.
410
     *
411
     * This allows for filtering based on both the value's "key" and "value" representation, which are identical.
412
     * It's particularly useful when the distinction between keys and values is relevant for the condition.
413
     *
414
     * @param (Closure(T, T): bool) $fn T
415
     *
416
     * @return MutableSet<T>
417
     */
418
    public function filterWithKey(Closure $fn): MutableSet
419
    {
420
        return $this->filter(static fn($k) => $fn($k, $k));
1✔
421
    }
422

423
    /**
424
     * Returns a `MutableSet` after an operation has been applied to each value
425
     * in the current `MutableSet`.
426
     *
427
     * Every value in the current Map is affected by a call to `map()`, unlike
428
     * `filter()` where only values that meet a certain criteria are affected.
429
     *
430
     * The keys will remain unchanged from the current `MutableSet` to the
431
     * returned `MutableSet`.
432
     *
433
     * @template Tu of array-key
434
     *
435
     * @param (Closure(T): Tu) $fn The callback containing the operation to apply to the current
436
     *                             `MutableSet` values.
437
     *
438
     * @return MutableSet<Tu> A `MutableSet` containing the values after a user-specified
439
     *                        operation is applied.
440
     */
441
    public function map(Closure $fn): MutableSet
442
    {
443
        return new MutableSet(Dict\map($this->elements, $fn));
2✔
444
    }
445

446
    /**
447
     * Transform the values of the current `MutableSet` by applying the provided callback,
448
     *  considering the value as both key and value.
449
     *
450
     * Similar to {@see MutableSet::map()}, this method extends the functionality by providing the value twice to the
451
     *  callback function: once as the "key" and once as the "value",
452
     *
453
     * The allows for transformations that take into account the value's dual role. It's useful for operations where the distinction
454
     *  between keys and values is relevant.
455
     *
456
     * @template Tu of array-key
457
     *
458
     * @param (Closure(T, T): Tu) $fn
459
     *
460
     * @return MutableSet<Tu>
461
     */
462
    public function mapWithKey(Closure $fn): MutableSet
463
    {
464
        return $this->map(static fn($k) => $fn($k, $k));
1✔
465
    }
466

467
    /**
468
     * Always throws an exception since `MutableSet` can only contain array-key values.
469
     *
470
     * @template Tu
471
     *
472
     * @param array<array-key, Tu> $elements The elements to use to combine with the elements of this `MutableSet`.
473
     *
474
     * @psalm-mutation-free
475
     *
476
     * @throws Exception\RuntimeException Always throws an exception since `MutableSet` can only contain array-key values.
477
     */
478
    public function zip(array $elements): never
479
    {
480
        throw new Exception\RuntimeException('Cannot zip a MutableSet.');
1✔
481
    }
482

483
    /**
484
     * Returns a `MutableSet` containing the first `n` values of the current
485
     * `MutableSet`.
486
     *
487
     * The returned `MutableSet` will always be a proper subset of the current
488
     * `MutableSet`.
489
     *
490
     * `$n` is 1-based. So the first element is 1, the second 2, etc.
491
     *
492
     * @param int<0, max> $n The last element that will be included in the returned
493
     *                       `MutableSet`.
494
     *
495
     * @return MutableSet<T> A `MutableSet` that is a proper subset of the current
496
     *                       `MutableSet` up to `n` elements.
497
     *
498
     * @psalm-mutation-free
499
     */
500
    public function take(int $n): MutableSet
501
    {
502
        return $this->slice(0, $n);
1✔
503
    }
504

505
    /**
506
     * Returns a `MutableSet` containing the values of the current `MutableSet`
507
     * up to but not including the first value that produces `false` when passed
508
     * to the specified callback.
509
     *
510
     * The returned `MutableSet` will always be a proper subset of the current
511
     * `MutableSet`.
512
     *
513
     * @param (Closure(T): bool) $fn The callback that is used to determine the stopping
514
     *                               condition.
515
     *
516
     * @return MutableSet<T> A `MutableSet` that is a proper subset of the current
517
     *                       `MutableSet` up until the callback returns `false`.
518
     */
519
    public function takeWhile(Closure $fn): MutableSet
520
    {
521
        return new MutableSet(Dict\take_while($this->elements, $fn));
1✔
522
    }
523

524
    /**
525
     * Returns a `MutableSet` containing the values after the `n`-th element of
526
     * the current `MutableSet`.
527
     *
528
     * The returned `MutableSet` will always be a proper subset of the current
529
     * `setInterface`.
530
     *
531
     * `$n` is 1-based. So the first element is 1, the second 2, etc.
532
     *
533
     * @param int<0, max> $n The last element to be skipped; the $n+1 element will be the
534
     *                       first one in the returned `MutableSet`.
535
     *
536
     * @return MutableSet<T> A `MutableSet` that is a proper subset of the current
537
     *                       `MutableSet` containing values after the specified `n`-th element.
538
     *
539
     * @psalm-mutation-free
540
     */
541
    public function drop(int $n): MutableSet
542
    {
543
        return $this->slice($n);
1✔
544
    }
545

546
    /**
547
     * Returns a `MutableSet` containing the values of the current `MutableSet`
548
     * starting after and including the first value that produces `true` when
549
     * passed to the specified callback.
550
     *
551
     * The returned `MutableSet` will always be a proper subset of the current
552
     * `MutableSet`.
553
     *
554
     * @param (Closure(T): bool) $fn The callback used to determine the starting element for the
555
     *                               returned `MutableSet`.
556
     *
557
     * @return MutableSet<T> A `MutableSet` that is a proper subset of the current
558
     *                       `MutableSet` starting after the callback returns `true`.
559
     */
560
    public function dropWhile(Closure $fn): MutableSet
561
    {
562
        return new MutableSet(Dict\drop_while($this->elements, $fn));
1✔
563
    }
564

565
    /**
566
     * Returns a subset of the current `MutableSet` starting from a given index up
567
     * to, but not including, the element at the provided length from the starting
568
     * index.
569
     *
570
     * `$start` is 0-based. $len is 1-based. So `slice(0, 2)` would return the
571
     * elements at index 0 and 1.
572
     *
573
     * The returned `MutableSet` will always be a proper subset of this
574
     * `MutableSet`.
575
     *
576
     * @param int<0, max> $start The starting index of this set to begin the returned
577
     *                           `MutableSet`.
578
     * @param int<0, max> $length The length of the returned `MutableSet`.
579
     *
580
     * @return MutableSet<T> A `MutableSet` that is a proper subset of the current
581
     *                       `MutableSet` starting at `$start` up to but not including
582
     *                       the element `$start + $length`.
583
     *
584
     * @psalm-mutation-free
585
     */
586
    public function slice(int $start, ?int $length = null): MutableSet
587
    {
588
        /** @psalm-suppress ImpureFunctionCall - conditionally pure */
589
        return MutableSet::fromArray(Dict\slice($this->elements, $start, $length));
3✔
590
    }
591

592
    /**
593
     * Returns a `MutableVector` containing the original `MutableSet` split into
594
     * chunks of the given size.
595
     *
596
     * If the original `MutableSet` doesn't divide evenly, the final chunk will be
597
     * smaller.
598
     *
599
     * @param positive-int $size The size of each chunk.
600
     *
601
     * @return MutableVector<MutableSet<T>> A `MutableVector` containing the original
602
     *                                      `MutableSet` split into chunks of the given size.
603
     *
604
     * @psalm-mutation-free
605
     */
606
    public function chunk(int $size): MutableVector
607
    {
608
        /**
609
         * @psalm-suppress MissingThrowsDocblock
610
         * @psalm-suppress ImpureFunctionCall
611
         */
612
        return MutableVector::fromArray(Vec\map(
1✔
613
            /**
614
             * @psalm-suppress MissingThrowsDocblock
615
             * @psalm-suppress ImpureFunctionCall
616
             */
617
            Vec\chunk($this->toArray(), $size),
1✔
618
            /**
619
             * @param list<T> $chunk
620
             *
621
             * @return MutableSet<T>
622
             */
623
            static fn(array $chunk) => MutableSet::fromArray($chunk)
1✔
624
        ));
1✔
625
    }
626

627
    /**
628
     * Determines if the specified offset exists in the current set.
629
     *
630
     * @param mixed $offset An offset to check for.
631
     *
632
     * @throws Exception\InvalidOffsetException If the offset type is not valid.
633
     *
634
     * @return bool Returns true if the specified offset exists, false otherwise.
635
     *
636
     * @psalm-mutation-free
637
     *
638
     * @psalm-assert array-key $offset
639
     */
640
    public function offsetExists(mixed $offset): bool
641
    {
642
        if (!is_int($offset) && !is_string($offset)) {
2✔
643
            throw new Exception\InvalidOffsetException('Invalid set read offset type, expected a string or an integer.');
1✔
644
        }
645

646
        /** @var T $offset - technically, we don't know if the offset is of type T, but we can assume it is, as this causes no "harm". */
647
        return $this->contains($offset);
1✔
648
    }
649

650
    /**
651
     * Returns the value at the specified offset.
652
     *
653
     * @param mixed $offset The offset to retrieve.
654
     *
655
     * @throws Exception\InvalidOffsetException If the offset type is not array-key.
656
     * @throws Exception\OutOfBoundsException If the offset does not exist.
657
     *
658
     * @return T The value at the specified offset.
659
     *
660
     * @psalm-mutation-free
661
     *
662
     * @psalm-assert array-key $offset
663
     */
664
    public function offsetGet(mixed $offset): mixed
665
    {
666
        if (!is_int($offset) && !is_string($offset)) {
2✔
667
            throw new Exception\InvalidOffsetException('Invalid set read offset type, expected a string or an integer.');
1✔
668
        }
669

670
        /** @var T $offset - technically, we don't know if the offset is of type T, but we can assume it is, as this causes no "harm". */
671
        return $this->at($offset);
1✔
672
    }
673

674
    /**
675
     * Sets the value at the specified offset.
676
     *
677
     * @param mixed $offset The offset to assign the value to.
678
     * @param T $value The value to set.
679
     *
680
     * @psalm-external-mutation-free
681
     *
682
     * @psalm-assert null|array-key $offset
683
     *
684
     * @throws Exception\InvalidOffsetException If the offset is not null or the value is not the same as the offset.
685
     */
686
    public function offsetSet(mixed $offset, mixed $value): void
687
    {
688
        if (null === $offset || $offset === $value) {
3✔
689
            $this->add($value);
1✔
690

691
            return;
1✔
692
        }
693

694
        throw new Exception\InvalidOffsetException('Invalid set write offset type, expected null or the same as the value.');
2✔
695
    }
696

697
    /**
698
     * Unsets the value at the specified offset.
699
     *
700
     * @param mixed $offset The offset to unset.
701
     *
702
     * @psalm-external-mutation-free
703
     *
704
     * @psalm-assert array-key $offset
705
     *
706
     * @throws Exception\InvalidOffsetException If the offset type is not valid.
707
     */
708
    public function offsetUnset(mixed $offset): void
709
    {
710
        if (!is_int($offset) && !is_string($offset)) {
2✔
711
            throw new Exception\InvalidOffsetException('Invalid set read offset type, expected a string or an integer.');
1✔
712
        }
713

714
        /** @var T $offset - technically, we don't know if the offset is of type T, but we can assume it is, as this causes no "harm". */
715
        $this->remove($offset);
1✔
716
    }
717
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc