Passed
Push — php74-serialize ( 4021bf...a2bdae )
by Marc
04:08
created

EnumSet::isEmpty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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