Passed
Push — doc-generics ( 55aaf3...ef6bd9 )
by Marc
03:16
created

EnumSet::__debugInfo()   A

Complexity

Conditions 1
Paths 1

Size

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