Passed
Push — doc-generics ( ebe4d6...7cd13d )
by Marc
03:57
created

EnumSet::removeIterable()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MabeEnum;
6
7
use Countable;
8
use InvalidArgumentException;
9
use Iterator;
10
use IteratorAggregate;
11
12
/**
13
 * A set of enumerators of the given enumeration (EnumSet<T extends Enum>)
14
 * based on an integer or binary bitset depending of given enumeration size.
15
 *
16
 * @template T of Enum
17
 * @implements IteratorAggregate<int, T>
18
 *
19
 * @copyright 2019 Marc Bennewitz
20
 * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
21
 * @link http://github.com/marc-mabe/php-enum for the canonical source repository
22
 */
23
class EnumSet implements IteratorAggregate, Countable
24
{
25
    /**
26
     * The classname of the Enumeration
27
     * @var class-string<T>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
28
     */
29
    private $enumeration;
0 ignored issues
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
30
31
    /**
32
     * Number of enumerators defined in the enumeration
33
     * @var int
34
     */
35
    private $enumerationCount;
36
37
    /**
38
     * Integer or binary (little endian) bitset
39
     * @var int|string
40
     */
41
    private $bitset = 0;
42
43
    /**
44
     * Integer or binary (little endian) empty bitset
45
     *
46
     * @var int|string
47
     */
48
    private $emptyBitset = 0;
49
50
    /**#@+
51
     * Defines private method names to be called depended of how the bitset type was set too.
52
     * ... Integer or binary bitset.
53
     * ... *Int or *Bin method
54
     *
55
     * @var string
56
     */
57
    /** @var string */
58
    private $fnDoGetIterator       = 'doGetIteratorInt';
0 ignored issues
show
Coding Style introduced by
Expected 1 blank line before member var; 8 found
Loading history...
59
60
    /** @var string */
61
    private $fnDoCount             = 'doCountInt';
62
63
    /** @var string */
64
    private $fnDoGetOrdinals       = 'doGetOrdinalsInt';
65
66
    /** @var string */
67
    private $fnDoGetBit            = 'doGetBitInt';
68
69
    /** @var string */
70
    private $fnDoSetBit            = 'doSetBitInt';
71
72
    /** @var string */
73
    private $fnDoUnsetBit          = 'doUnsetBitInt';
74
75
    /** @var string */
76
    private $fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeInt';
77
78
    /** @var string */
79
    private $fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeInt';
80
    /**#@-*/
81
82
    /**
83
     * Constructor
84
     *
85
     * @param class-string<T> $enumeration The classname of the enumeration
0 ignored issues
show
Coding Style introduced by
Expected 42 spaces after parameter type; 1 found
Loading history...
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
86
     * @param iterable<T|null|bool|int|float|string|array<mixed>>|null $enumerators iterable list of enumerators initializing the set
0 ignored issues
show
introduced by
Parameter comment must start with a capital letter
Loading history...
87
     * @throws InvalidArgumentException
0 ignored issues
show
introduced by
Comment missing for @throws tag in function comment
Loading history...
88
     */
89 104
    public function __construct(string $enumeration, iterable $enumerators = null)
90
    {
91 104
        if (!\is_subclass_of($enumeration, Enum::class)) {
92 1
            throw new InvalidArgumentException(\sprintf(
93 1
                '%s can handle subclasses of %s only',
94 1
                __METHOD__,
95 1
                Enum::class
96
            ));
97
        }
98
99 103
        $this->enumeration      = $enumeration;
100 103
        $this->enumerationCount = \count($enumeration::getConstants());
101
102
        // By default the bitset is initialized as integer bitset
103
        // in case the enumeration has more enumerators then integer bits
104
        // we will switch this into a binary bitset
105 103
        if ($this->enumerationCount > \PHP_INT_SIZE * 8) {
106
            // init binary bitset with zeros
107 25
            $this->bitset = $this->emptyBitset = \str_repeat("\0", (int)\ceil($this->enumerationCount / 8));
108
109
            // switch internal binary bitset functions
110 25
            $this->fnDoGetIterator       = 'doGetIteratorBin';
111 25
            $this->fnDoCount             = 'doCountBin';
112 25
            $this->fnDoGetOrdinals       = 'doGetOrdinalsBin';
113 25
            $this->fnDoGetBit            = 'doGetBitBin';
114 25
            $this->fnDoSetBit            = 'doSetBitBin';
115 25
            $this->fnDoUnsetBit          = 'doUnsetBitBin';
116 25
            $this->fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeBin';
117 25
            $this->fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeBin';
118
        }
119
120 103
        if ($enumerators !== null) {
121 23
            foreach ($enumerators as $enumerator) {
122 23
                $this->{$this->fnDoSetBit}($enumeration::get($enumerator)->getOrdinal());
123
            }
124
        }
125 103
    }
126
127
    /**
128
     * Get the classname of the enumeration
129
     * @return class-string<T>
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
130
     */
131 2
    public function getEnumeration(): string
132
    {
133 2
        return $this->enumeration;
134
    }
135
136
    /* write access (mutable) */
137
138
    /**
139
     * Adds an enumerator object or value
140
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
0 ignored issues
show
Bug introduced by
The type MabeEnum\T was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
141
     * @return void
142
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
143
     */
144 16
    public function add($enumerator): void
145
    {
146 16
        $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
147 16
    }
148
149
    /**
150
     * Adds all enumerator objects or values of the given iterable
151
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators Iterable list of enumerator objects or values
152
     * @return void
153
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
154
     */
155 11
    public function addIterable(iterable $enumerators): void
156
    {
157 11
        $bitset = $this->bitset;
158
159
        try {
160 11
            foreach ($enumerators as $enumerator) {
161 11
                $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
162
            }
163 1
        } catch (\Throwable $e) {
164
            // reset all changes until error happened
165 1
            $this->bitset = $bitset;
166 1
            throw $e;
167
        }
168 10
    }
169
170
    /**
171
     * Removes the given enumerator object or value
172
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
173
     * @return void
174
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
175
     */
176 8
    public function remove($enumerator): void
177
    {
178 8
        $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
179 8
    }
180
181
    /**
182
     * Adds an enumerator object or value
183
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
184
     * @return void
185
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
186
     * @see add()
187
     * @see with()
188
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
189
     */
190 1
    public function attach($enumerator): void
191
    {
192 1
        $this->add($enumerator);
193 1
    }
194
195
    /**
196
     * Removes the given enumerator object or value
197
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
198
     * @return void
199
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
200
     * @see remove()
201
     * @see without()
202
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
203
     */
204 1
    public function detach($enumerator): void
205
    {
206 1
        $this->remove($enumerator);
207 1
    }
208
209
    /**
210
     * Removes all enumerator objects or values of the given iterable
211
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators Iterable list of enumerator objects or values
212
     * @return void
213
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
214
     */
215 11
    public function removeIterable(iterable $enumerators): void
216
    {
217 11
        $bitset = $this->bitset;
218
219
        try {
220 11
            foreach ($enumerators as $enumerator) {
221 11
                $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
222
            }
223 1
        } catch (\Throwable $e) {
224
            // reset all changes until error happened
225 1
            $this->bitset = $bitset;
226 1
            throw $e;
227
        }
228 10
    }
229
230
    /**
231
     * Modify this set from both this and other (this | other)
232
     *
233
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the union
234
     * @return void
235
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
236
     */
237 4
    public function setUnion(EnumSet $other): void
238
    {
239 4
        if ($this->enumeration !== $other->enumeration) {
240 1
            throw new InvalidArgumentException(\sprintf(
241 1
                'Other should be of the same enumeration as this %s',
242 1
                $this->enumeration
243
            ));
244
        }
245
246 3
        $this->bitset = $this->bitset | $other->bitset;
247 3
    }
248
249
    /**
250
     * Modify this set with enumerators common to both this and other (this & other)
251
     *
252
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the intersect
253
     * @return void
254
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
255
     */
256 4
    public function setIntersect(EnumSet $other): void
257
    {
258 4
        if ($this->enumeration !== $other->enumeration) {
259 1
            throw new InvalidArgumentException(\sprintf(
260 1
                'Other should be of the same enumeration as this %s',
261 1
                $this->enumeration
262
            ));
263
        }
264
265 3
        $this->bitset = $this->bitset & $other->bitset;
266 3
    }
267
268
    /**
269
     * Modify this set with enumerators in this but not in other (this - other)
270
     *
271
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the diff
272
     * @return void
273
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
274
     */
275 4
    public function setDiff(EnumSet $other): void
276
    {
277 4
        if ($this->enumeration !== $other->enumeration) {
278 1
            throw new InvalidArgumentException(\sprintf(
279 1
                'Other should be of the same enumeration as this %s',
280 1
                $this->enumeration
281
            ));
282
        }
283
284 3
        $this->bitset = $this->bitset & ~$other->bitset;
285 3
    }
286
287
    /**
288
     * Modify this set with enumerators in either this and other but not in both (this ^ other)
289
     *
290
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the symmetric difference
291
     * @return void
292
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
293
     */
294 4
    public function setSymDiff(EnumSet $other): void
295
    {
296 4
        if ($this->enumeration !== $other->enumeration) {
297 1
            throw new InvalidArgumentException(\sprintf(
298 1
                'Other should be of the same enumeration as this %s',
299 1
                $this->enumeration
300
            ));
301
        }
302
303 3
        $this->bitset = $this->bitset ^ $other->bitset;
304 3
    }
305
306
    /**
307
     * Set the given binary bitset in little-endian order
308
     *
309
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
310
     * @return void
311
     * @throws InvalidArgumentException On out-of-range bits given as input bitset
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
312
     * @uses doSetBinaryBitsetLeBin()
313
     * @uses doSetBinaryBitsetLeInt()
314
     */
315 1
    public function setBinaryBitsetLe(string $bitset): void
316
    {
317 1
        $this->{$this->fnDoSetBinaryBitsetLe}($bitset);
318 1
    }
319
320
    /**
321
     * Set binary bitset in little-endian order
322
     *
323
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
324
     * @return void
325
     * @throws InvalidArgumentException On out-of-range bits given as input bitset
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
326
     * @see setBinaryBitsetLeBin()
327
     * @see doSetBinaryBitsetLeInt()
328
     */
329 9
    private function doSetBinaryBitsetLeBin($bitset): void
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $bitset
Loading history...
330
    {
331
        /** @var string $thisBitset */
332 9
        $thisBitset = $this->bitset;
333
334 9
        $size   = \strlen($thisBitset);
335 9
        $sizeIn = \strlen($bitset);
336
337 9
        if ($sizeIn < $size) {
338
            // add "\0" if the given bitset is not long enough
339 1
            $bitset .= \str_repeat("\0", $size - $sizeIn);
340 8
        } elseif ($sizeIn > $size) {
341 2
            if (\ltrim(\substr($bitset, $size), "\0") !== '') {
342 1
                throw new InvalidArgumentException('out-of-range bits detected');
343
            }
344 1
            $bitset = \substr($bitset, 0, $size);
345
        }
346
347
        // truncate out-of-range bits of last byte
348 8
        $lastByteMaxOrd = $this->enumerationCount % 8;
349 8
        if ($lastByteMaxOrd !== 0) {
350 8
            $lastByte         = $bitset[-1];
351 8
            $lastByteExpected = \chr((1 << $lastByteMaxOrd) - 1) & $lastByte;
352 8
            if ($lastByte !== $lastByteExpected) {
353 2
                throw new InvalidArgumentException('out-of-range bits detected');
354
            }
355
356 6
            $this->bitset = \substr($bitset, 0, -1) . $lastByteExpected;
357
        }
358
359 6
        $this->bitset = $bitset;
360 6
    }
361
362
    /**
363
     * Set binary bitset in little-endian order
364
     *
365
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
366
     * @return void
367
     * @throws InvalidArgumentException On out-of-range bits given as input bitset
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
368
     * @see setBinaryBitsetLeBin()
369
     * @see doSetBinaryBitsetLeBin()
370
     */
371 5
    private function doSetBinaryBitsetLeInt($bitset): void
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $bitset
Loading history...
372
    {
373 5
        $len = \strlen($bitset);
374 5
        $int = 0;
375 5
        for ($i = 0; $i < $len; ++$i) {
376 5
            $ord = \ord($bitset[$i]);
377
378 5
            if ($ord && $i > \PHP_INT_SIZE - 1) {
379 1
                throw new InvalidArgumentException('out-of-range bits detected');
380
            }
381
382 5
            $int |= $ord << (8 * $i);
383
        }
384
385 4
        if ($int & (~0 << $this->enumerationCount)) {
386 2
            throw new InvalidArgumentException('out-of-range bits detected');
387
        }
388
389 2
        $this->bitset = $int;
390 2
    }
391
392
    /**
393
     * Set the given binary bitset in big-endian order
394
     *
395
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
396
     * @return void
397
     * @throws InvalidArgumentException On out-of-range bits given as input bitset
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
398
     */
399 1
    public function setBinaryBitsetBe(string $bitset): void
400
    {
401 1
        $this->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset));
402 1
    }
403
404
    /**
405
     * Set a bit at the given ordinal number
406
     *
407
     * @param int $ordinal Ordinal number of bit to set
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
408
     * @param bool $bit    The bit to set
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 4 found
Loading history...
409
     * @return void
410
     * @throws InvalidArgumentException If the given ordinal number is out-of-range
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
411
     * @uses doSetBitBin()
412
     * @uses doSetBitInt()
413
     * @uses doUnsetBitBin()
414
     * @uses doUnsetBitInt()
415
     */
416 3
    public function setBit(int $ordinal, bool $bit): void
417
    {
418 3
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
419 1
            throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $this instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
420
        }
421
422 2
        if ($bit) {
423 2
            $this->{$this->fnDoSetBit}($ordinal);
424
        } else {
425 2
            $this->{$this->fnDoUnsetBit}($ordinal);
426
        }
427 2
    }
428
429
    /**
430
     * Set a bit at the given ordinal number.
431
     *
432
     * This is the binary bitset implementation.
433
     *
434
     * @param int $ordinal Ordinal number of bit to set
435
     * @return void
436
     * @see setBit()
437
     * @see doSetBitInt()
438
     */
439 16
    private function doSetBitBin($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
440
    {
441
        /** @var string $thisBitset */
442 16
        $thisBitset = $this->bitset;
443
444 16
        $byte = (int) ($ordinal / 8);
445 16
        $thisBitset[$byte] = $thisBitset[$byte] | \chr(1 << ($ordinal % 8));
446
447 16
        $this->bitset = $thisBitset;
448 16
    }
449
450
    /**
451
     * Set a bit at the given ordinal number.
452
     *
453
     * This is the binary bitset implementation.
454
     *
455
     * @param int $ordinal Ordinal number of bit to set
456
     * @return void
457
     * @see setBit()
458
     * @see doSetBitBin()
459
     */
460 63
    private function doSetBitInt($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
461
    {
462
        /** @var int $thisBitset */
463 63
        $thisBitset = $this->bitset;
464 63
        $thisBitset = $thisBitset | (1 << $ordinal);
465
466 63
        $this->bitset = $thisBitset;
467 63
    }
468
469
    /**
470
     * Unset a bit at the given ordinal number.
471
     *
472
     * This is the binary bitset implementation.
473
     *
474
     * @param int $ordinal Ordinal number of bit to unset
475
     * @return void
476
     * @see setBit()
477
     * @see doUnsetBitInt()
478
     */
479 12
    private function doUnsetBitBin($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
480
    {
481
        /** @var string $thisBitset */
482 12
        $thisBitset = $this->bitset;
483
484 12
        $byte = (int) ($ordinal / 8);
485 12
        $thisBitset[$byte] = $thisBitset[$byte] & \chr(~(1 << ($ordinal % 8)));
486
487 12
        $this->bitset = $thisBitset;
488 12
    }
489
490
    /**
491
     * Unset a bit at the given ordinal number.
492
     *
493
     * This is the integer bitset implementation.
494
     *
495
     * @param int $ordinal Ordinal number of bit to unset
496
     * @return void
497
     * @see setBit()
498
     * @see doUnsetBitBin()
499
     */
500 23
    private function doUnsetBitInt($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
501
    {
502 23
        $this->bitset = $this->bitset & ~(1 << $ordinal);
503 23
    }
504
505
    /* write access (immutable) */
506
507
    /**
508
     * Creates a new set with the given enumerator object or value added
509
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
510
     * @return static<T>
511
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
512
     */
513 28
    public function with($enumerator): self
514
    {
515 28
        $clone = clone $this;
516 28
        $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
517 28
        return $clone;
518
    }
519
520
    /**
521
     * Creates a new set with the given enumeration objects or values added
522
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators Iterable list of enumerator objects or values
523
     * @return static<T>
524
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
525
     */
526 5
    public function withIterable(iterable $enumerators): self
527
    {
528 5
        $clone = clone $this;
529 5
        foreach ($enumerators as $enumerator) {
530 5
            $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
531
        }
532 5
        return $clone;
533
    }
534
535
    /**
536
     * Create a new set with the given enumerator object or value removed
537
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
538
     * @return static<T>
539
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
540
     */
541 9
    public function without($enumerator): self
542
    {
543 9
        $clone = clone $this;
544 9
        $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
545 9
        return $clone;
546
    }
547
548
    /**
549
     * Creates a new set with the given enumeration objects or values removed
550
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators Iterable list of enumerator objects or values
551
     * @return static<T>
552
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
553
     */
554 5
    public function withoutIterable(iterable $enumerators): self
555
    {
556 5
        $clone = clone $this;
557 5
        foreach ($enumerators as $enumerator) {
558 5
            $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
559
        }
560 5
        return $clone;
561
    }
562
563
    /**
564
     * Create a new set with enumerators from both this and other (this | other)
565
     *
566
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the union
567
     * @return static<T>
568
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
569
     */
570 2
    public function withUnion(EnumSet $other): self
571
    {
572 2
        $clone = clone $this;
573 2
        $clone->setUnion($other);
574 2
        return $clone;
575
    }
576
577
    /**
578
     * Create a new set with enumerators from both this and other (this | other)
579
     *
580
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the union
581
     * @return static<T>
582
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
583
     * @see withUnion()
584
     * @see setUnion()
585
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
586
     */
587 1
    public function union(EnumSet $other): self
588
    {
589 1
        return $this->withUnion($other);
590
    }
591
592
    /**
593
     * Create a new set with enumerators common to both this and other (this & other)
594
     *
595
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the intersect
596
     * @return static<T>
597
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
598
     */
599 2
    public function withIntersect(EnumSet $other): self
600
    {
601 2
        $clone = clone $this;
602 2
        $clone->setIntersect($other);
603 2
        return $clone;
604
    }
605
606
    /**
607
     * Create a new set with enumerators common to both this and other (this & other)
608
     *
609
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the intersect
610
     * @return static<T>
611
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
612
     * @see withIntersect()
613
     * @see setIntersect()
614
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
615
     */
616 1
    public function intersect(EnumSet $other): self
617
    {
618 1
        return $this->withIntersect($other);
619
    }
620
621
    /**
622
     * Create a new set with enumerators in this but not in other (this - other)
623
     *
624
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the diff
625
     * @return static<T>
626
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
627
     */
628 2
    public function withDiff(EnumSet $other): self
629
    {
630 2
        $clone = clone $this;
631 2
        $clone->setDiff($other);
632 2
        return $clone;
633
    }
634
635
    /**
636
     * Create a new set with enumerators in this but not in other (this - other)
637
     *
638
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the diff
639
     * @return static<T>
640
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
641
     * @see withDiff()
642
     * @see setDiff()
643
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
644
     */
645 1
    public function diff(EnumSet $other): self
646
    {
647 1
        return $this->withDiff($other);
648
    }
649
650
    /**
651
     * Create a new set with enumerators in either this and other but not in both (this ^ other)
652
     *
653
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the symmetric difference
654
     * @return static<T>
655
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
656
     */
657 2
    public function withSymDiff(EnumSet $other): self
658
    {
659 2
        $clone = clone $this;
660 2
        $clone->setSymDiff($other);
661 2
        return $clone;
662
    }
663
664
    /**
665
     * Create a new set with enumerators in either this and other but not in both (this ^ other)
666
     *
667
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the symmetric difference
668
     * @return static<T>
669
     * @throws InvalidArgumentException If $other doesn't match the enumeration
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
670
     * @see withSymDiff()
671
     * @see setSymDiff()
672
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
673
     */
674 1
    public function symDiff(EnumSet $other): self
675
    {
676 1
        return $this->withSymDiff($other);
677
    }
678
679
    /**
680
     * Create a new set with the given binary bitset in little-endian order
681
     *
682
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
683
     * @return static<T>
684
     * @throws InvalidArgumentException On out-of-range bits given as input bitset
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
685
     * @uses doSetBinaryBitsetLeBin()
686
     * @uses doSetBinaryBitsetLeInt()
687
     */
688 11
    public function withBinaryBitsetLe(string $bitset): self
689
    {
690 11
        $clone = clone $this;
691 11
        $clone->{$this->fnDoSetBinaryBitsetLe}($bitset);
692 5
        return $clone;
693
    }
694
695
    /**
696
     * Create a new set with the given binary bitset in big-endian order
697
     *
698
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
699
     * @return static<T>
700
     * @throws InvalidArgumentException On out-of-range bits given as input bitset
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
701
     */
702 1
    public function withBinaryBitsetBe(string $bitset): self
703
    {
704 1
        $clone = $this;
705 1
        $clone->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset));
706 1
        return $clone;
707
    }
708
709
    /**
710
     * Create a new set with the bit at the given ordinal number set
711
     *
712
     * @param int $ordinal Ordinal number of bit to set
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
713
     * @param bool $bit    The bit to set
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter name; 4 found
Loading history...
714
     * @return static<T>
715
     * @throws InvalidArgumentException If the given ordinal number is out-of-range
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
716
     * @uses doSetBitBin()
717
     * @uses doSetBitInt()
718
     * @uses doUnsetBitBin()
719
     * @uses doUnsetBitInt()
720
     */
721 1
    public function withBit(int $ordinal, bool $bit): self
722
    {
723 1
        $clone = clone $this;
724 1
        $clone->setBit($ordinal, $bit);
725 1
        return $clone;
726
    }
727
728
    /* read access */
729
730
    /**
731
     * Test if the given enumerator exists
732
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
733
     * @return bool
734
     */
735 24
    public function has($enumerator): bool
736
    {
737 24
        return $this->{$this->fnDoGetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
738
    }
739
740
    /**
741
     * Test if the given enumerator exists
742
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
743
     * @return bool
744
     * @see has()
745
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
746
     */
747 1
    public function contains($enumerator): bool
748
    {
749 1
        return $this->has($enumerator);
750
    }
751
752
    /* IteratorAggregate */
753
754
    /**
755
     * Get a new iterator
756
     * @return Iterator<T>
757
     * @uses doGetIteratorInt()
758
     * @uses doGetIteratorBin()
759
     */
760 13
    public function getIterator(): Iterator
761
    {
762 13
        return $this->{$this->fnDoGetIterator}();
763
    }
764
765
    /**
766
     * Get a new Iterator.
767
     *
768
     * This is the binary bitset implementation.
769
     *
770
     * @return Iterator<T>
771
     * @see getIterator()
772
     * @see goGetIteratorInt()
773
     */
774 4
    private function doGetIteratorBin()
775
    {
776
        /** @var string $bitset */
777 4
        $bitset   = $this->bitset;
778 4
        $byteLen  = \strlen($bitset);
779 4
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
780 4
            if ($bitset[$bytePos] === "\0") {
781
                // fast skip null byte
782 4
                continue;
783
            }
784
785 4
            $ord = \ord($bitset[$bytePos]);
786 4
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
787 4
                if ($ord & (1 << $bitPos)) {
788 4
                    $ordinal = $bytePos * 8 + $bitPos;
789 4
                    yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
790
                }
791
            }
792
        }
793 4
    }
794
795
    /**
796
     * Get a new Iterator.
797
     *
798
     * This is the integer bitset implementation.
799
     *
800
     * @return Iterator<T>
801
     * @see getIterator()
802
     * @see doGetIteratorBin()
803
     */
804 9
    private function doGetIteratorInt()
805
    {
806
        /** @var int $bitset */
807 9
        $bitset = $this->bitset;
808 9
        $count  = $this->enumerationCount;
809 9
        for ($ordinal = 0; $ordinal < $count; ++$ordinal) {
810 9
            if ($bitset & (1 << $ordinal)) {
811 7
                yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
812
            }
813
        }
814 7
    }
815
816
    /* Countable */
817
818
    /**
819
     * Count the number of elements
820
     *
821
     * @return int
822
     * @uses doCountBin()
823
     * @uses doCountInt()
824
     */
825 32
    public function count(): int
826
    {
827 32
        return $this->{$this->fnDoCount}();
828
    }
829
830
    /**
831
     * Count the number of elements.
832
     *
833
     * This is the binary bitset implementation.
834
     *
835
     * @return int
836
     * @see count()
837
     * @see doCountInt()
838
     */
839 15
    private function doCountBin()
840
    {
841
        /** @var string $bitset */
842 15
        $bitset  = $this->bitset;
843 15
        $count   = 0;
844 15
        $byteLen = \strlen($bitset);
845 15
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
846 15
            if ($bitset[$bytePos] === "\0") {
847
                // fast skip null byte
848 15
                continue;
849
            }
850
851 15
            $ord = \ord($bitset[$bytePos]);
852 15
            if ($ord & 0b00000001) ++$count;
853 15
            if ($ord & 0b00000010) ++$count;
854 15
            if ($ord & 0b00000100) ++$count;
855 15
            if ($ord & 0b00001000) ++$count;
856 15
            if ($ord & 0b00010000) ++$count;
857 15
            if ($ord & 0b00100000) ++$count;
858 15
            if ($ord & 0b01000000) ++$count;
859 15
            if ($ord & 0b10000000) ++$count;
860
        }
861 15
        return $count;
862
    }
863
864
    /**
865
     * Count the number of elements.
866
     *
867
     * This is the integer bitset implementation.
868
     *
869
     * @return int
870
     * @see count()
871
     * @see doCountBin()
872
     */
873 17
    private function doCountInt()
874
    {
875
        /** @var int $bitset */
876 17
        $bitset = $this->bitset;
877 17
        $count  = 0;
878
879
        // PHP does not support right shift unsigned
880 17
        if ($bitset < 0) {
881 5
            $count  = 1;
882 5
            $bitset = $bitset & \PHP_INT_MAX;
883
        }
884
885
        // iterate byte by byte and count set bits
886 17
        $phpIntBitSize = \PHP_INT_SIZE * 8;
887 17
        for ($bitPos = 0; $bitPos < $phpIntBitSize; $bitPos += 8) {
888 17
            $bitChk = 0xff << $bitPos;
889 17
            $byte = $bitset & $bitChk;
890 17
            if ($byte) {
891 16
                $byte = $byte >> $bitPos;
892 16
                if ($byte & 0b00000001) ++$count;
893 16
                if ($byte & 0b00000010) ++$count;
894 16
                if ($byte & 0b00000100) ++$count;
895 16
                if ($byte & 0b00001000) ++$count;
896 16
                if ($byte & 0b00010000) ++$count;
897 16
                if ($byte & 0b00100000) ++$count;
898 16
                if ($byte & 0b01000000) ++$count;
899 16
                if ($byte & 0b10000000) ++$count;
900
            }
901
902 17
            if ($bitset <= $bitChk) {
903 17
                break;
904
            }
905
        }
906
907 17
        return $count;
908
    }
909
910
    /**
911
     * Check if this EnumSet is the same as other
912
     * @param EnumSet<T> $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
913
     * @return bool
914
     */
915 3
    public function isEqual(EnumSet $other): bool
916
    {
917 3
        return $this->enumeration === $other->enumeration
918 3
            && $this->bitset === $other->bitset;
919
    }
920
921
    /**
922
     * Check if this EnumSet is a subset of other
923
     * @param EnumSet<T> $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
924
     * @return bool
925
     */
926 4
    public function isSubset(EnumSet $other): bool
927
    {
928 4
        return $this->enumeration === $other->enumeration
929 4
            && ($this->bitset & $other->bitset) === $this->bitset;
930
    }
931
932
    /**
933
     * Check if this EnumSet is a superset of other
934
     * @param EnumSet<T> $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
935
     * @return bool
936
     */
937 4
    public function isSuperset(EnumSet $other): bool
938
    {
939 4
        return $this->enumeration === $other->enumeration
940 4
            && ($this->bitset | $other->bitset) === $this->bitset;
941
    }
942
943
    /**
944
     * Tests if the set is empty
945
     *
946
     * @return bool
947
     */
948 5
    public function isEmpty(): bool
949
    {
950 5
        return $this->bitset === $this->emptyBitset;
951
    }
952
953
    /**
954
     * Get ordinal numbers of the defined enumerators as array
955
     * @return int[]
956
     * @uses  doGetOrdinalsBin()
957
     * @uses  doGetOrdinalsInt()
958
     */
959 18
    public function getOrdinals(): array
960
    {
961 18
        return $this->{$this->fnDoGetOrdinals}();
962
    }
963
964
    /**
965
     * Get ordinal numbers of the defined enumerators as array.
966
     *
967
     * This is the binary bitset implementation.
968
     *
969
     * @return int[]
970
     * @see getOrdinals()
971
     * @see goGetOrdinalsInt()
972
     */
973 1
    private function doGetOrdinalsBin()
974
    {
975
        /** @var string $bitset */
976 1
        $bitset   = $this->bitset;
977 1
        $ordinals = [];
978 1
        $byteLen  = \strlen($bitset);
979 1
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
980 1
            if ($bitset[$bytePos] === "\0") {
981
                // fast skip null byte
982 1
                continue;
983
            }
984
985 1
            $ord = \ord($bitset[$bytePos]);
986 1
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
987 1
                if ($ord & (1 << $bitPos)) {
988 1
                    $ordinals[] = $bytePos * 8 + $bitPos;
989
                }
990
            }
991
        }
992 1
        return $ordinals;
993
    }
994
995
    /**
996
     * Get ordinal numbers of the defined enumerators as array.
997
     *
998
     * This is the integer bitset implementation.
999
     *
1000
     * @return int[]
1001
     * @see getOrdinals()
1002
     * @see doGetOrdinalsBin()
1003
     */
1004 17
    private function doGetOrdinalsInt()
1005
    {
1006
        /** @var int $bitset */
1007 17
        $bitset   = $this->bitset;
1008 17
        $ordinals = [];
1009 17
        $count    = $this->enumerationCount;
1010 17
        for ($ordinal = 0; $ordinal < $count; ++$ordinal) {
1011 17
            if ($bitset & (1 << $ordinal)) {
1012 17
                $ordinals[] = $ordinal;
1013
            }
1014
        }
1015 17
        return $ordinals;
1016
    }
1017
1018
    /**
1019
     * Get values of the defined enumerators as array
1020
     * @return (null|bool|int|float|string|array<mixed>)[]
1021
     */
1022 13
    public function getValues(): array
1023
    {
1024 13
        $enumeration = $this->enumeration;
1025 13
        $values      = [];
1026 13
        foreach ($this->getOrdinals() as $ord) {
1027 13
            $values[] = $enumeration::byOrdinal($ord)->getValue();
1028
        }
1029 13
        return $values;
1030
    }
1031
1032
    /**
1033
     * Get names of the defined enumerators as array
1034
     * @return string[]
1035
     */
1036 1
    public function getNames(): array
1037
    {
1038 1
        $enumeration = $this->enumeration;
1039 1
        $names       = [];
1040 1
        foreach ($this->getOrdinals() as $ord) {
1041 1
            $names[] = $enumeration::byOrdinal($ord)->getName();
1042
        }
1043 1
        return $names;
1044
    }
1045
1046
    /**
1047
     * Get the defined enumerators as array
1048
     * @return T[]
1049
     */
1050 2
    public function getEnumerators(): array
1051
    {
1052 2
        $enumeration = $this->enumeration;
1053 2
        $enumerators = [];
1054 2
        foreach ($this->getOrdinals() as $ord) {
1055 2
            $enumerators[] = $enumeration::byOrdinal($ord);
1056
        }
1057 2
        return $enumerators;
1058
    }
1059
1060
    /**
1061
     * Get binary bitset in little-endian order
1062
     *
1063
     * @return string
1064
     * @uses doGetBinaryBitsetLeBin()
1065
     * @uses doGetBinaryBitsetLeInt()
1066
     */
1067 6
    public function getBinaryBitsetLe(): string
1068
    {
1069 6
        return $this->{$this->fnDoGetBinaryBitsetLe}();
1070
    }
1071
1072
    /**
1073
     * Get binary bitset in little-endian order.
1074
     *
1075
     * This is the binary bitset implementation.
1076
     *
1077
     * @return string
1078
     * @see getBinaryBitsetLe()
1079
     * @see doGetBinaryBitsetLeInt()
1080
     */
1081 4
    private function doGetBinaryBitsetLeBin()
1082
    {
1083 4
        return $this->bitset;
1084
    }
1085
1086
    /**
1087
     * Get binary bitset in little-endian order.
1088
     *
1089
     * This is the integer bitset implementation.
1090
     *
1091
     * @return string
1092
     * @see getBinaryBitsetLe()
1093
     * @see doGetBinaryBitsetLeBin()
1094
     */
1095 2
    private function doGetBinaryBitsetLeInt()
1096
    {
1097 2
        $bin = \pack(\PHP_INT_SIZE === 8 ? 'P' : 'V', $this->bitset);
1098 2
        return \substr($bin, 0, (int)\ceil($this->enumerationCount / 8));
1099
    }
1100
1101
    /**
1102
     * Get binary bitset in big-endian order
1103
     *
1104
     * @return string
1105
     */
1106 1
    public function getBinaryBitsetBe(): string
1107
    {
1108 1
        return \strrev($this->bitset);
1109
    }
1110
1111
    /**
1112
     * Get a bit at the given ordinal number
1113
     *
1114
     * @param int $ordinal Ordinal number of bit to get
1115
     * @return bool
1116
     * @throws InvalidArgumentException If the given ordinal number is out-of-range
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
1117
     * @uses doGetBitBin()
1118
     * @uses doGetBitInt()
1119
     */
1120 4
    public function getBit(int $ordinal): bool
1121
    {
1122 4
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
1123 1
            throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $this instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
1124
        }
1125
1126 3
        return $this->{$this->fnDoGetBit}($ordinal);
1127
    }
1128
1129
    /**
1130
     * Get a bit at the given ordinal number.
1131
     *
1132
     * This is the binary bitset implementation.
1133
     *
1134
     * @param int $ordinal Ordinal number of bit to get
1135
     * @return bool
1136
     * @see getBit()
1137
     * @see doGetBitInt()
1138
     */
1139 8
    private function doGetBitBin($ordinal)
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
1140
    {
1141
        /** @var string $bitset */
1142 8
        $bitset = $this->bitset;
1143
1144 8
        return (\ord($bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
1145
    }
1146
1147
    /**
1148
     * Get a bit at the given ordinal number.
1149
     *
1150
     * This is the integer bitset implementation.
1151
     *
1152
     * @param int $ordinal Ordinal number of bit to get
1153
     * @return bool
1154
     * @see getBit()
1155
     * @see doGetBitBin()
1156
     */
1157 18
    private function doGetBitInt($ordinal)
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
1158
    {
1159
        /** @var int $bitset */
1160 18
        $bitset = $this->bitset;
1161
1162 18
        return (bool)($bitset & (1 << $ordinal));
1163
    }
1164
}
1165