EnumSet::with()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 of 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 2020, Marc Bennewitz
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
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 106
    public function __construct(string $enumeration, iterable $enumerators = null)
90
    {
91 106
        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 105
        $this->enumeration      = $enumeration;
100 105
        $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 105
        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 105
        if ($enumerators !== null) {
121 24
            foreach ($enumerators as $enumerator) {
122 24
                $this->{$this->fnDoSetBit}($enumeration::get($enumerator)->getOrdinal());
123
            }
124
        }
125 105
    }
126
127
    /**
128
     * Add virtual private property "__enumerators" with a list of enumerator values set
129
     * to the result of var_dump.
130
     *
131
     * This helps debugging as internally the enumerators of this EnumSet gets stored
132
     * as either integer or binary bit-array.
133
     *
134
     * @return array<string, mixed>
135
     */
136 1
    public function __debugInfo() {
137 1
        $dbg = (array)$this;
138 1
        $dbg["\0" . self::class . "\0__enumerators"] = $this->getValues();
139 1
        return $dbg;
140
    }
141
142
    /**
143
     * Get the classname of the enumeration
144
     * @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...
145
     */
146 2
    public function getEnumeration(): string
147
    {
148 2
        return $this->enumeration;
149
    }
150
151
    /* write access (mutable) */
152
153
    /**
154
     * Adds an enumerator object or value
155
     * @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...
156
     * @return void
157
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
158
     */
159 18
    public function add($enumerator): void
160
    {
161 18
        $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
162 18
    }
163
164
    /**
165
     * Adds all enumerator objects or values of the given iterable
166
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators Iterable list of enumerator objects or values
167
     * @return void
168
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
169
     */
170 11
    public function addIterable(iterable $enumerators): void
171
    {
172 11
        $bitset = $this->bitset;
173
174
        try {
175 11
            foreach ($enumerators as $enumerator) {
176 11
                $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
177
            }
178 1
        } catch (\Throwable $e) {
179
            // reset all changes until error happened
180 1
            $this->bitset = $bitset;
181 1
            throw $e;
182
        }
183 10
    }
184
185
    /**
186
     * Removes the given enumerator object or value
187
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
188
     * @return void
189
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
190
     */
191 10
    public function remove($enumerator): void
192
    {
193 10
        $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
194 10
    }
195
196
    /**
197
     * Adds an enumerator object or value
198
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
199
     * @return void
200
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
201
     * @see add()
202
     * @see with()
203
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
204
     */
205 1
    public function attach($enumerator): void
206
    {
207 1
        $this->add($enumerator);
208 1
    }
209
210
    /**
211
     * Removes the given enumerator object or value
212
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
213
     * @return void
214
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
215
     * @see remove()
216
     * @see without()
217
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
218
     */
219 1
    public function detach($enumerator): void
220
    {
221 1
        $this->remove($enumerator);
222 1
    }
223
224
    /**
225
     * Removes all enumerator objects or values of the given iterable
226
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators Iterable list of enumerator objects or values
227
     * @return void
228
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
229
     */
230 11
    public function removeIterable(iterable $enumerators): void
231
    {
232 11
        $bitset = $this->bitset;
233
234
        try {
235 11
            foreach ($enumerators as $enumerator) {
236 11
                $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
237
            }
238 1
        } catch (\Throwable $e) {
239
            // reset all changes until error happened
240 1
            $this->bitset = $bitset;
241 1
            throw $e;
242
        }
243 10
    }
244
245
    /**
246
     * Modify this set from both this and other (this | other)
247
     *
248
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the union
249
     * @return void
250
     * @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...
251
     */
252 4
    public function setUnion(EnumSet $other): void
253
    {
254 4
        if ($this->enumeration !== $other->enumeration) {
255 1
            throw new InvalidArgumentException(\sprintf(
256 1
                'Other should be of the same enumeration as this %s',
257 1
                $this->enumeration
258
            ));
259
        }
260
261 3
        $this->bitset = $this->bitset | $other->bitset;
262 3
    }
263
264
    /**
265
     * Modify this set with enumerators common to both this and other (this & other)
266
     *
267
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the intersect
268
     * @return void
269
     * @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...
270
     */
271 4
    public function setIntersect(EnumSet $other): void
272
    {
273 4
        if ($this->enumeration !== $other->enumeration) {
274 1
            throw new InvalidArgumentException(\sprintf(
275 1
                'Other should be of the same enumeration as this %s',
276 1
                $this->enumeration
277
            ));
278
        }
279
280 3
        $this->bitset = $this->bitset & $other->bitset;
281 3
    }
282
283
    /**
284
     * Modify this set with enumerators in this but not in other (this - other)
285
     *
286
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the diff
287
     * @return void
288
     * @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...
289
     */
290 4
    public function setDiff(EnumSet $other): void
291
    {
292 4
        if ($this->enumeration !== $other->enumeration) {
293 1
            throw new InvalidArgumentException(\sprintf(
294 1
                'Other should be of the same enumeration as this %s',
295 1
                $this->enumeration
296
            ));
297
        }
298
299 3
        $this->bitset = $this->bitset & ~$other->bitset;
300 3
    }
301
302
    /**
303
     * Modify this set with enumerators in either this and other but not in both (this ^ other)
304
     *
305
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the symmetric difference
306
     * @return void
307
     * @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...
308
     */
309 4
    public function setSymDiff(EnumSet $other): void
310
    {
311 4
        if ($this->enumeration !== $other->enumeration) {
312 1
            throw new InvalidArgumentException(\sprintf(
313 1
                'Other should be of the same enumeration as this %s',
314 1
                $this->enumeration
315
            ));
316
        }
317
318 3
        $this->bitset = $this->bitset ^ $other->bitset;
319 3
    }
320
321
    /**
322
     * Set the given binary bitset in little-endian order
323
     *
324
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
325
     * @return void
326
     * @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...
327
     * @uses doSetBinaryBitsetLeBin()
328
     * @uses doSetBinaryBitsetLeInt()
329
     */
330 1
    public function setBinaryBitsetLe(string $bitset): void
331
    {
332 1
        $this->{$this->fnDoSetBinaryBitsetLe}($bitset);
333 1
    }
334
335
    /**
336
     * Set binary bitset in little-endian order
337
     *
338
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
339
     * @return void
340
     * @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...
341
     * @see setBinaryBitsetLeBin()
342
     * @see doSetBinaryBitsetLeInt()
343
     */
344 9
    private function doSetBinaryBitsetLeBin($bitset): void
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $bitset
Loading history...
345
    {
346
        /** @var string $thisBitset */
347 9
        $thisBitset = $this->bitset;
348
349 9
        $size   = \strlen($thisBitset);
350 9
        $sizeIn = \strlen($bitset);
351
352 9
        if ($sizeIn < $size) {
353
            // add "\0" if the given bitset is not long enough
354 1
            $bitset .= \str_repeat("\0", $size - $sizeIn);
355 8
        } elseif ($sizeIn > $size) {
356 2
            if (\ltrim(\substr($bitset, $size), "\0") !== '') {
357 1
                throw new InvalidArgumentException('out-of-range bits detected');
358
            }
359 1
            $bitset = \substr($bitset, 0, $size);
360
        }
361
362
        // truncate out-of-range bits of last byte
363 8
        $lastByteMaxOrd = $this->enumerationCount % 8;
364 8
        if ($lastByteMaxOrd !== 0) {
365 8
            $lastByte         = $bitset[-1];
366 8
            $lastByteExpected = \chr((1 << $lastByteMaxOrd) - 1) & $lastByte;
367 8
            if ($lastByte !== $lastByteExpected) {
368 2
                throw new InvalidArgumentException('out-of-range bits detected');
369
            }
370
371 6
            $this->bitset = \substr($bitset, 0, -1) . $lastByteExpected;
372
        }
373
374 6
        $this->bitset = $bitset;
375 6
    }
376
377
    /**
378
     * Set binary bitset in little-endian order
379
     *
380
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
381
     * @return void
382
     * @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...
383
     * @see setBinaryBitsetLeBin()
384
     * @see doSetBinaryBitsetLeBin()
385
     */
386 5
    private function doSetBinaryBitsetLeInt($bitset): void
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $bitset
Loading history...
387
    {
388 5
        $len = \strlen($bitset);
389 5
        $int = 0;
390 5
        for ($i = 0; $i < $len; ++$i) {
391 5
            $ord = \ord($bitset[$i]);
392
393 5
            if ($ord && $i > \PHP_INT_SIZE - 1) {
394 1
                throw new InvalidArgumentException('out-of-range bits detected');
395
            }
396
397 5
            $int |= $ord << (8 * $i);
398
        }
399
400 4
        if ($int & (~0 << $this->enumerationCount)) {
401 2
            throw new InvalidArgumentException('out-of-range bits detected');
402
        }
403
404 2
        $this->bitset = $int;
405 2
    }
406
407
    /**
408
     * Set the given binary bitset in big-endian order
409
     *
410
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
411
     * @return void
412
     * @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...
413
     */
414 1
    public function setBinaryBitsetBe(string $bitset): void
415
    {
416 1
        $this->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset));
417 1
    }
418
419
    /**
420
     * Set a bit at the given ordinal number
421
     *
422
     * @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...
423
     * @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...
424
     * @return void
425
     * @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...
426
     * @uses doSetBitBin()
427
     * @uses doSetBitInt()
428
     * @uses doUnsetBitBin()
429
     * @uses doUnsetBitInt()
430
     */
431 3
    public function setBit(int $ordinal, bool $bit): void
432
    {
433 3
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
434 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...
435
        }
436
437 2
        if ($bit) {
438 2
            $this->{$this->fnDoSetBit}($ordinal);
439
        } else {
440 2
            $this->{$this->fnDoUnsetBit}($ordinal);
441
        }
442 2
    }
443
444
    /**
445
     * Set a bit at the given ordinal number.
446
     *
447
     * This is the binary bitset implementation.
448
     *
449
     * @param int $ordinal Ordinal number of bit to set
450
     * @return void
451
     * @see setBit()
452
     * @see doSetBitInt()
453
     */
454 16
    private function doSetBitBin($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
455
    {
456
        /** @var string $thisBitset */
457 16
        $thisBitset = $this->bitset;
458
459 16
        $byte = (int) ($ordinal / 8);
460 16
        $thisBitset[$byte] = $thisBitset[$byte] | \chr(1 << ($ordinal % 8));
461
462 16
        $this->bitset = $thisBitset;
463 16
    }
464
465
    /**
466
     * Set a bit at the given ordinal number.
467
     *
468
     * This is the binary bitset implementation.
469
     *
470
     * @param int $ordinal Ordinal number of bit to set
471
     * @return void
472
     * @see setBit()
473
     * @see doSetBitBin()
474
     */
475 65
    private function doSetBitInt($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
476
    {
477
        /** @var int $thisBitset */
478 65
        $thisBitset = $this->bitset;
479
480 65
        $this->bitset = $thisBitset | (1 << $ordinal);
481 65
    }
482
483
    /**
484
     * Unset a bit at the given ordinal number.
485
     *
486
     * This is the binary bitset implementation.
487
     *
488
     * @param int $ordinal Ordinal number of bit to unset
489
     * @return void
490
     * @see setBit()
491
     * @see doUnsetBitInt()
492
     */
493 12
    private function doUnsetBitBin($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
494
    {
495
        /** @var string $thisBitset */
496 12
        $thisBitset = $this->bitset;
497
498 12
        $byte = (int) ($ordinal / 8);
499 12
        $thisBitset[$byte] = $thisBitset[$byte] & \chr(~(1 << ($ordinal % 8)));
500
501 12
        $this->bitset = $thisBitset;
502 12
    }
503
504
    /**
505
     * Unset a bit at the given ordinal number.
506
     *
507
     * This is the integer bitset implementation.
508
     *
509
     * @param int $ordinal Ordinal number of bit to unset
510
     * @return void
511
     * @see setBit()
512
     * @see doUnsetBitBin()
513
     */
514 24
    private function doUnsetBitInt($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
515
    {
516
        /** @var int $thisBitset */
517 24
        $thisBitset = $this->bitset;
518
519 24
        $this->bitset = $thisBitset & ~(1 << $ordinal);
520 24
    }
521
522
    /* write access (immutable) */
523
524
    /**
525
     * Creates a new set with the given enumerator object or value added
526
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
527
     * @return static
528
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
529
     */
530 27
    public function with($enumerator): self
531
    {
532 27
        $clone = clone $this;
533 27
        $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
534 27
        return $clone;
535
    }
536
537
    /**
538
     * Creates a new set with the given enumeration objects or values added
539
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators Iterable list of enumerator objects or values
540
     * @return static
541
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
542
     */
543 5
    public function withIterable(iterable $enumerators): self
544
    {
545 5
        $clone = clone $this;
546 5
        foreach ($enumerators as $enumerator) {
547 5
            $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
548
        }
549 5
        return $clone;
550
    }
551
552
    /**
553
     * Create a new set with the given enumerator object or value removed
554
     * @param T|null|bool|int|float|string|array<mixed> $enumerator Enumerator object or value
555
     * @return static
556
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
557
     */
558 8
    public function without($enumerator): self
559
    {
560 8
        $clone = clone $this;
561 8
        $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
562 8
        return $clone;
563
    }
564
565
    /**
566
     * Creates a new set with the given enumeration objects or values removed
567
     * @param iterable<T|null|bool|int|float|string|array<mixed>> $enumerators Iterable list of enumerator objects or values
568
     * @return static
569
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
570
     */
571 5
    public function withoutIterable(iterable $enumerators): self
572
    {
573 5
        $clone = clone $this;
574 5
        foreach ($enumerators as $enumerator) {
575 5
            $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
576
        }
577 5
        return $clone;
578
    }
579
580
    /**
581
     * Create a new set with enumerators from both this and other (this | other)
582
     *
583
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the union
584
     * @return static
585
     * @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...
586
     */
587 2
    public function withUnion(EnumSet $other): self
588
    {
589 2
        $clone = clone $this;
590 2
        $clone->setUnion($other);
591 2
        return $clone;
592
    }
593
594
    /**
595
     * Create a new set with enumerators from both this and other (this | other)
596
     *
597
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the union
598
     * @return static
599
     * @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...
600
     * @see withUnion()
601
     * @see setUnion()
602
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
603
     */
604 1
    public function union(EnumSet $other): self
605
    {
606 1
        return $this->withUnion($other);
607
    }
608
609
    /**
610
     * Create a new set with enumerators common to both this and other (this & other)
611
     *
612
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the intersect
613
     * @return static
614
     * @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...
615
     */
616 2
    public function withIntersect(EnumSet $other): self
617
    {
618 2
        $clone = clone $this;
619 2
        $clone->setIntersect($other);
620 2
        return $clone;
621
    }
622
623
    /**
624
     * Create a new set with enumerators common to both this and other (this & other)
625
     *
626
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the intersect
627
     * @return static
628
     * @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...
629
     * @see withIntersect()
630
     * @see setIntersect()
631
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
632
     */
633 1
    public function intersect(EnumSet $other): self
634
    {
635 1
        return $this->withIntersect($other);
636
    }
637
638
    /**
639
     * Create a new set with enumerators in this but not in other (this - other)
640
     *
641
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the diff
642
     * @return static
643
     * @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...
644
     */
645 2
    public function withDiff(EnumSet $other): self
646
    {
647 2
        $clone = clone $this;
648 2
        $clone->setDiff($other);
649 2
        return $clone;
650
    }
651
652
    /**
653
     * Create a new set with enumerators in this but not in other (this - other)
654
     *
655
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the diff
656
     * @return static
657
     * @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...
658
     * @see withDiff()
659
     * @see setDiff()
660
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
661
     */
662 1
    public function diff(EnumSet $other): self
663
    {
664 1
        return $this->withDiff($other);
665
    }
666
667
    /**
668
     * Create a new set with enumerators in either this and other but not in both (this ^ other)
669
     *
670
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the symmetric difference
671
     * @return static
672
     * @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...
673
     */
674 2
    public function withSymDiff(EnumSet $other): self
675
    {
676 2
        $clone = clone $this;
677 2
        $clone->setSymDiff($other);
678 2
        return $clone;
679
    }
680
681
    /**
682
     * Create a new set with enumerators in either this and other but not in both (this ^ other)
683
     *
684
     * @param EnumSet<T> $other EnumSet of the same enumeration to produce the symmetric difference
685
     * @return static
686
     * @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...
687
     * @see withSymDiff()
688
     * @see setSymDiff()
689
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
690
     */
691 1
    public function symDiff(EnumSet $other): self
692
    {
693 1
        return $this->withSymDiff($other);
694
    }
695
696
    /**
697
     * Create a new set with the given binary bitset in little-endian order
698
     *
699
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
700
     * @return static
701
     * @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...
702
     * @uses doSetBinaryBitsetLeBin()
703
     * @uses doSetBinaryBitsetLeInt()
704
     */
705 11
    public function withBinaryBitsetLe(string $bitset): self
706
    {
707 11
        $clone = clone $this;
708 11
        $clone->{$this->fnDoSetBinaryBitsetLe}($bitset);
709 5
        return $clone;
710
    }
711
712
    /**
713
     * Create a new set with the given binary bitset in big-endian order
714
     *
715
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
716
     * @return static
717
     * @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...
718
     */
719 1
    public function withBinaryBitsetBe(string $bitset): self
720
    {
721 1
        $clone = $this;
722 1
        $clone->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset));
723 1
        return $clone;
724
    }
725
726
    /**
727
     * Create a new set with the bit at the given ordinal number set
728
     *
729
     * @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...
730
     * @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...
731
     * @return static
732
     * @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...
733
     * @uses doSetBitBin()
734
     * @uses doSetBitInt()
735
     * @uses doUnsetBitBin()
736
     * @uses doUnsetBitInt()
737
     */
738 1
    public function withBit(int $ordinal, bool $bit): self
739
    {
740 1
        $clone = clone $this;
741 1
        $clone->setBit($ordinal, $bit);
742 1
        return $clone;
743
    }
744
745
    /* read access */
746
747
    /**
748
     * Test if the given enumerator exists
749
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
750
     * @return bool
751
     */
752 25
    public function has($enumerator): bool
753
    {
754 25
        return $this->{$this->fnDoGetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
755
    }
756
757
    /**
758
     * Test if the given enumerator exists
759
     * @param T|null|bool|int|float|string|array<mixed> $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
760
     * @return bool
761
     * @see has()
762
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
763
     */
764 1
    public function contains($enumerator): bool
765
    {
766 1
        return $this->has($enumerator);
767
    }
768
769
    /* IteratorAggregate */
770
771
    /**
772
     * Get a new iterator
773
     * @return Iterator<int, T>
774
     * @uses doGetIteratorInt()
775
     * @uses doGetIteratorBin()
776
     */
777 13
    public function getIterator(): Iterator
778
    {
779 13
        return $this->{$this->fnDoGetIterator}();
780
    }
781
782
    /**
783
     * Get a new Iterator.
784
     *
785
     * This is the binary bitset implementation.
786
     *
787
     * @return Iterator<int, T>
788
     * @see getIterator()
789
     * @see goGetIteratorInt()
790
     */
791 4
    private function doGetIteratorBin()
792
    {
793
        /** @var string $bitset */
794 4
        $bitset   = $this->bitset;
795 4
        $byteLen  = \strlen($bitset);
796 4
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
797 4
            if ($bitset[$bytePos] === "\0") {
798
                // fast skip null byte
799 4
                continue;
800
            }
801
802 4
            $ord = \ord($bitset[$bytePos]);
803 4
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
804 4
                if ($ord & (1 << $bitPos)) {
805 4
                    $ordinal = $bytePos * 8 + $bitPos;
806 4
                    yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
807
                }
808
            }
809
        }
810 4
    }
811
812
    /**
813
     * Get a new Iterator.
814
     *
815
     * This is the integer bitset implementation.
816
     *
817
     * @return Iterator<int, T>
818
     * @see getIterator()
819
     * @see doGetIteratorBin()
820
     */
821 9
    private function doGetIteratorInt()
822
    {
823
        /** @var int $bitset */
824 9
        $bitset = $this->bitset;
825 9
        $count  = $this->enumerationCount;
826 9
        for ($ordinal = 0; $ordinal < $count; ++$ordinal) {
827 9
            if ($bitset & (1 << $ordinal)) {
828 7
                yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
829
            }
830
        }
831 7
    }
832
833
    /* Countable */
834
835
    /**
836
     * Count the number of elements
837
     *
838
     * @return int
839
     * @uses doCountBin()
840
     * @uses doCountInt()
841
     */
842 33
    public function count(): int
843
    {
844 33
        return $this->{$this->fnDoCount}();
845
    }
846
847
    /**
848
     * Count the number of elements.
849
     *
850
     * This is the binary bitset implementation.
851
     *
852
     * @return int
853
     * @see count()
854
     * @see doCountInt()
855
     */
856 15
    private function doCountBin()
857
    {
858
        /** @var string $bitset */
859 15
        $bitset  = $this->bitset;
860 15
        $count   = 0;
861 15
        $byteLen = \strlen($bitset);
862 15
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
863 15
            if ($bitset[$bytePos] === "\0") {
864
                // fast skip null byte
865 15
                continue;
866
            }
867
868 15
            $ord = \ord($bitset[$bytePos]);
869 15
            if ($ord & 0b00000001) ++$count;
870 15
            if ($ord & 0b00000010) ++$count;
871 15
            if ($ord & 0b00000100) ++$count;
872 15
            if ($ord & 0b00001000) ++$count;
873 15
            if ($ord & 0b00010000) ++$count;
874 15
            if ($ord & 0b00100000) ++$count;
875 15
            if ($ord & 0b01000000) ++$count;
876 15
            if ($ord & 0b10000000) ++$count;
877
        }
878 15
        return $count;
879
    }
880
881
    /**
882
     * Count the number of elements.
883
     *
884
     * This is the integer bitset implementation.
885
     *
886
     * @return int
887
     * @see count()
888
     * @see doCountBin()
889
     */
890 18
    private function doCountInt()
891
    {
892
        /** @var int $bitset */
893 18
        $bitset = $this->bitset;
894 18
        $count  = 0;
895
896
        // PHP does not support right shift unsigned
897 18
        if ($bitset < 0) {
898 5
            $count  = 1;
899 5
            $bitset = $bitset & \PHP_INT_MAX;
900
        }
901
902
        // iterate byte by byte and count set bits
903 18
        $phpIntBitSize = \PHP_INT_SIZE * 8;
904 18
        for ($bitPos = 0; $bitPos < $phpIntBitSize; $bitPos += 8) {
905 18
            $bitChk = 0xff << $bitPos;
906 18
            $byte = $bitset & $bitChk;
907 18
            if ($byte) {
908 17
                $byte = $byte >> $bitPos;
909 17
                if ($byte & 0b00000001) ++$count;
910 17
                if ($byte & 0b00000010) ++$count;
911 17
                if ($byte & 0b00000100) ++$count;
912 17
                if ($byte & 0b00001000) ++$count;
913 17
                if ($byte & 0b00010000) ++$count;
914 17
                if ($byte & 0b00100000) ++$count;
915 17
                if ($byte & 0b01000000) ++$count;
916 17
                if ($byte & 0b10000000) ++$count;
917
            }
918
919 18
            if ($bitset <= $bitChk) {
920 18
                break;
921
            }
922
        }
923
924 18
        return $count;
925
    }
926
927
    /**
928
     * Check if this EnumSet is the same as other
929
     * @param EnumSet<T> $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
930
     * @return bool
931
     */
932 3
    public function isEqual(EnumSet $other): bool
933
    {
934 3
        return $this->enumeration === $other->enumeration
935 3
            && $this->bitset === $other->bitset;
936
    }
937
938
    /**
939
     * Check if this EnumSet is a subset of other
940
     * @param EnumSet<T> $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
941
     * @return bool
942
     */
943 4
    public function isSubset(EnumSet $other): bool
944
    {
945 4
        return $this->enumeration === $other->enumeration
946 4
            && ($this->bitset & $other->bitset) === $this->bitset;
947
    }
948
949
    /**
950
     * Check if this EnumSet is a superset of other
951
     * @param EnumSet<T> $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
952
     * @return bool
953
     */
954 4
    public function isSuperset(EnumSet $other): bool
955
    {
956 4
        return $this->enumeration === $other->enumeration
957 4
            && ($this->bitset | $other->bitset) === $this->bitset;
958
    }
959
960
    /**
961
     * Tests if the set is empty
962
     *
963
     * @return bool
964
     */
965 5
    public function isEmpty(): bool
966
    {
967 5
        return $this->bitset === $this->emptyBitset;
968
    }
969
970
    /**
971
     * Get ordinal numbers of the defined enumerators as array
972
     * @return array<int, int>
973
     * @uses  doGetOrdinalsBin()
974
     * @uses  doGetOrdinalsInt()
975
     */
976 19
    public function getOrdinals(): array
977
    {
978 19
        return $this->{$this->fnDoGetOrdinals}();
979
    }
980
981
    /**
982
     * Get ordinal numbers of the defined enumerators as array.
983
     *
984
     * This is the binary bitset implementation.
985
     *
986
     * @return array<int, int>
987
     * @see getOrdinals()
988
     * @see goGetOrdinalsInt()
989
     */
990 1
    private function doGetOrdinalsBin()
991
    {
992
        /** @var string $bitset */
993 1
        $bitset   = $this->bitset;
994 1
        $ordinals = [];
995 1
        $byteLen  = \strlen($bitset);
996 1
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
997 1
            if ($bitset[$bytePos] === "\0") {
998
                // fast skip null byte
999 1
                continue;
1000
            }
1001
1002 1
            $ord = \ord($bitset[$bytePos]);
1003 1
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
1004 1
                if ($ord & (1 << $bitPos)) {
1005 1
                    $ordinals[] = $bytePos * 8 + $bitPos;
1006
                }
1007
            }
1008
        }
1009 1
        return $ordinals;
1010
    }
1011
1012
    /**
1013
     * Get ordinal numbers of the defined enumerators as array.
1014
     *
1015
     * This is the integer bitset implementation.
1016
     *
1017
     * @return array<int, int>
1018
     * @see getOrdinals()
1019
     * @see doGetOrdinalsBin()
1020
     */
1021 18
    private function doGetOrdinalsInt()
1022
    {
1023
        /** @var int $bitset */
1024 18
        $bitset   = $this->bitset;
1025 18
        $ordinals = [];
1026 18
        $count    = $this->enumerationCount;
1027 18
        for ($ordinal = 0; $ordinal < $count; ++$ordinal) {
1028 18
            if ($bitset & (1 << $ordinal)) {
1029 18
                $ordinals[] = $ordinal;
1030
            }
1031
        }
1032 18
        return $ordinals;
1033
    }
1034
1035
    /**
1036
     * Get values of the defined enumerators as array
1037
     * @return (null|bool|int|float|string|array)[]
1038
     *
1039
     * @phpstan-return array<int, null|bool|int|float|string|array<mixed>>
1040
     * @psalm-return list<null|bool|int|float|string|array>
1041
     */
1042 14
    public function getValues(): array
1043
    {
1044 14
        $enumeration = $this->enumeration;
1045 14
        $values      = [];
1046 14
        foreach ($this->getOrdinals() as $ord) {
1047 14
            $values[] = $enumeration::byOrdinal($ord)->getValue();
1048
        }
1049 14
        return $values;
1050
    }
1051
1052
    /**
1053
     * Get names of the defined enumerators as array
1054
     * @return string[]
1055
     *
1056
     * @phpstan-return array<int, string>
1057
     * @psalm-return list<string>
1058
     */
1059 1
    public function getNames(): array
1060
    {
1061 1
        $enumeration = $this->enumeration;
1062 1
        $names       = [];
1063 1
        foreach ($this->getOrdinals() as $ord) {
1064 1
            $names[] = $enumeration::byOrdinal($ord)->getName();
1065
        }
1066 1
        return $names;
1067
    }
1068
1069
    /**
1070
     * Get the defined enumerators as array
1071
     * @return Enum[]
1072
     *
1073
     * @phpstan-return array<int, T>
1074
     * @psalm-return list<T>
1075
     */
1076 2
    public function getEnumerators(): array
1077
    {
1078 2
        $enumeration = $this->enumeration;
1079 2
        $enumerators = [];
1080 2
        foreach ($this->getOrdinals() as $ord) {
1081 2
            $enumerators[] = $enumeration::byOrdinal($ord);
1082
        }
1083 2
        return $enumerators;
1084
    }
1085
1086
    /**
1087
     * Get binary bitset in little-endian order
1088
     *
1089
     * @return string
1090
     * @uses doGetBinaryBitsetLeBin()
1091
     * @uses doGetBinaryBitsetLeInt()
1092
     */
1093 8
    public function getBinaryBitsetLe(): string
1094
    {
1095 8
        return $this->{$this->fnDoGetBinaryBitsetLe}();
1096
    }
1097
1098
    /**
1099
     * Get binary bitset in little-endian order.
1100
     *
1101
     * This is the binary bitset implementation.
1102
     *
1103
     * @return string
1104
     * @see getBinaryBitsetLe()
1105
     * @see doGetBinaryBitsetLeInt()
1106
     */
1107 5
    private function doGetBinaryBitsetLeBin()
1108
    {
1109
        /** @var string $bitset */
1110 5
        $bitset = $this->bitset;
1111
1112 5
        return $bitset;
1113
    }
1114
1115
    /**
1116
     * Get binary bitset in little-endian order.
1117
     *
1118
     * This is the integer bitset implementation.
1119
     *
1120
     * @return string
1121
     * @see getBinaryBitsetLe()
1122
     * @see doGetBinaryBitsetLeBin()
1123
     */
1124 3
    private function doGetBinaryBitsetLeInt()
1125
    {
1126 3
        $bin = \pack(\PHP_INT_SIZE === 8 ? 'P' : 'V', $this->bitset);
1127 3
        return \substr($bin, 0, (int)\ceil($this->enumerationCount / 8));
1128
    }
1129
1130
    /**
1131
     * Get binary bitset in big-endian order
1132
     *
1133
     * @return string
1134
     */
1135 2
    public function getBinaryBitsetBe(): string
1136
    {
1137 2
        return \strrev($this->getBinaryBitsetLe());
1138
    }
1139
1140
    /**
1141
     * Get a bit at the given ordinal number
1142
     *
1143
     * @param int $ordinal Ordinal number of bit to get
1144
     * @return bool
1145
     * @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...
1146
     * @uses doGetBitBin()
1147
     * @uses doGetBitInt()
1148
     */
1149 4
    public function getBit(int $ordinal): bool
1150
    {
1151 4
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
1152 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...
1153
        }
1154
1155 3
        return $this->{$this->fnDoGetBit}($ordinal);
1156
    }
1157
1158
    /**
1159
     * Get a bit at the given ordinal number.
1160
     *
1161
     * This is the binary bitset implementation.
1162
     *
1163
     * @param int $ordinal Ordinal number of bit to get
1164
     * @return bool
1165
     * @see getBit()
1166
     * @see doGetBitInt()
1167
     */
1168 8
    private function doGetBitBin($ordinal)
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
1169
    {
1170
        /** @var string $bitset */
1171 8
        $bitset = $this->bitset;
1172
1173 8
        return (\ord($bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
1174
    }
1175
1176
    /**
1177
     * Get a bit at the given ordinal number.
1178
     *
1179
     * This is the integer bitset implementation.
1180
     *
1181
     * @param int $ordinal Ordinal number of bit to get
1182
     * @return bool
1183
     * @see getBit()
1184
     * @see doGetBitBin()
1185
     */
1186 19
    private function doGetBitInt($ordinal)
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
1187
    {
1188
        /** @var int $bitset */
1189 19
        $bitset = $this->bitset;
1190
1191 19
        return (bool)($bitset & (1 << $ordinal));
1192
    }
1193
}
1194