Test Failed
Push — doc-generics ( ebe4d6 )
by Marc
03:56
created

EnumSet::__construct()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 5

Importance

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