Passed
Push — master ( 93b7bb...5c048f )
by Marc
01:43 queued 11s
created

EnumSet::__debugInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 4
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MabeEnum;
6
7
use Countable;
8
use InvalidArgumentException;
9
use Iterator;
10
use IteratorAggregate;
11
12
/**
13
 * A set of enumerators of the given enumeration (EnumSet<T 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 106
    public function __construct(string $enumeration, iterable $enumerators = null)
72
    {
73 106
        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 105
        $this->enumeration      = $enumeration;
82 105
        $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 105
        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 105
        if ($enumerators !== null) {
103 24
            foreach ($enumerators as $enumerator) {
104 24
                $this->{$this->fnDoSetBit}($enumeration::get($enumerator)->getOrdinal());
105
            }
106
        }
107 105
    }
108
109
    /**
110
     * Add virtual private property "__enumerators" with a list of enumerator values set
111
     * to the result of var_dump.
112
     *
113
     * This helps debugging as internally the enumerators of this EnumSet gets stored
114
     * as either integer or binary bit-array.
115
     *
116
     * @return array<string, mixed>
117
     */
118 1
    public function __debugInfo() {
119 1
        $dbg = (array)$this;
120 1
        $dbg["\0" . self::class . "\0__enumerators"] = $this->getValues();
121 1
        return $dbg;
122
    }
123
124
    /**
125
     * Get the classname of the enumeration
126
     * @return string
127
     */
128 2
    public function getEnumeration(): string
129
    {
130 2
        return $this->enumeration;
131
    }
132
133
    /* write access (mutable) */
134
135
    /**
136
     * Adds an enumerator object or value
137
     * @param Enum|null|bool|int|float|string|array $enumerator Enumerator object or value
138
     * @return void
139
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
140
     */
141 18
    public function add($enumerator): void
142
    {
143 18
        $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
144 18
    }
145
146
    /**
147
     * Adds all enumerator objects or values of the given iterable
148
     * @param iterable $enumerators Iterable list of enumerator objects or values
149
     * @return void
150
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
151
     */
152 11
    public function addIterable(iterable $enumerators): void
153
    {
154 11
        $bitset = $this->bitset;
155
156
        try {
157 11
            foreach ($enumerators as $enumerator) {
158 11
                $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
159
            }
160 1
        } catch (\Throwable $e) {
161
            // reset all changes until error happened
162 1
            $this->bitset = $bitset;
163 1
            throw $e;
164
        }
165 10
    }
166
167
    /**
168
     * Removes the given enumerator object or value
169
     * @param Enum|null|bool|int|float|string|array $enumerator Enumerator object or value
170
     * @return void
171
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
172
     */
173 10
    public function remove($enumerator): void
174
    {
175 10
        $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
176 10
    }
177
178
    /**
179
     * Adds an enumerator object or value
180
     * @param Enum|null|bool|int|float|string|array $enumerator Enumerator object or value
181
     * @return void
182
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
183
     * @see add()
184
     * @see with()
185
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
186
     */
187 1
    public function attach($enumerator): void
188
    {
189 1
        $this->add($enumerator);
190 1
    }
191
192
    /**
193
     * Removes the given enumerator object or value
194
     * @param Enum|null|bool|int|float|string|array $enumerator Enumerator object or value
195
     * @return void
196
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
197
     * @see remove()
198
     * @see without()
199
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
200
     */
201 1
    public function detach($enumerator): void
202
    {
203 1
        $this->remove($enumerator);
204 1
    }
205
206
    /**
207
     * Removes all enumerator objects or values of the given iterable
208
     * @param iterable $enumerators Iterable list of enumerator objects or values
209
     * @return void
210
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
211
     */
212 11
    public function removeIterable(iterable $enumerators): void
213
    {
214 11
        $bitset = $this->bitset;
215
216
        try {
217 11
            foreach ($enumerators as $enumerator) {
218 11
                $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
219
            }
220 1
        } catch (\Throwable $e) {
221
            // reset all changes until error happened
222 1
            $this->bitset = $bitset;
223 1
            throw $e;
224
        }
225 10
    }
226
227
    /**
228
     * Modify this set from both this and other (this | other)
229
     *
230
     * @param EnumSet $other EnumSet of the same enumeration to produce the union
231
     * @return void
232
     * @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...
233
     */
234 4
    public function setUnion(EnumSet $other): void
235
    {
236 4
        if ($this->enumeration !== $other->enumeration) {
237 1
            throw new InvalidArgumentException(\sprintf(
238 1
                'Other should be of the same enumeration as this %s',
239 1
                $this->enumeration
240
            ));
241
        }
242
243 3
        $this->bitset = $this->bitset | $other->bitset;
244 3
    }
245
246
    /**
247
     * Modify this set with enumerators common to both this and other (this & other)
248
     *
249
     * @param EnumSet $other EnumSet of the same enumeration to produce the intersect
250
     * @return void
251
     * @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...
252
     */
253 4
    public function setIntersect(EnumSet $other): void
254
    {
255 4
        if ($this->enumeration !== $other->enumeration) {
256 1
            throw new InvalidArgumentException(\sprintf(
257 1
                'Other should be of the same enumeration as this %s',
258 1
                $this->enumeration
259
            ));
260
        }
261
262 3
        $this->bitset = $this->bitset & $other->bitset;
263 3
    }
264
265
    /**
266
     * Modify this set with enumerators in this but not in other (this - other)
267
     *
268
     * @param EnumSet $other EnumSet of the same enumeration to produce the diff
269
     * @return void
270
     * @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...
271
     */
272 4
    public function setDiff(EnumSet $other): void
273
    {
274 4
        if ($this->enumeration !== $other->enumeration) {
275 1
            throw new InvalidArgumentException(\sprintf(
276 1
                'Other should be of the same enumeration as this %s',
277 1
                $this->enumeration
278
            ));
279
        }
280
281 3
        $this->bitset = $this->bitset & ~$other->bitset;
282 3
    }
283
284
    /**
285
     * Modify this set with enumerators in either this and other but not in both (this ^ other)
286
     *
287
     * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference
288
     * @return void
289
     * @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...
290
     */
291 4
    public function setSymDiff(EnumSet $other): void
292
    {
293 4
        if ($this->enumeration !== $other->enumeration) {
294 1
            throw new InvalidArgumentException(\sprintf(
295 1
                'Other should be of the same enumeration as this %s',
296 1
                $this->enumeration
297
            ));
298
        }
299
300 3
        $this->bitset = $this->bitset ^ $other->bitset;
301 3
    }
302
303
    /**
304
     * Set the given binary bitset in little-endian order
305
     *
306
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
307
     * @return void
308
     * @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...
309
     * @uses doSetBinaryBitsetLeBin()
310
     * @uses doSetBinaryBitsetLeInt()
311
     */
312 1
    public function setBinaryBitsetLe(string $bitset): void
313
    {
314 1
        $this->{$this->fnDoSetBinaryBitsetLe}($bitset);
315 1
    }
316
317
    /**
318
     * Set binary bitset in little-endian order
319
     *
320
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
321
     * @return void
322
     * @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...
323
     * @see setBinaryBitsetLeBin()
324
     * @see doSetBinaryBitsetLeInt()
325
     */
326 9
    private function doSetBinaryBitsetLeBin($bitset): void
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $bitset
Loading history...
327
    {
328 9
        $size   = \strlen($this->bitset);
329 9
        $sizeIn = \strlen($bitset);
330
331 9
        if ($sizeIn < $size) {
332
            // add "\0" if the given bitset is not long enough
333 1
            $bitset .= \str_repeat("\0", $size - $sizeIn);
334 8
        } elseif ($sizeIn > $size) {
335 2
            if (\ltrim(\substr($bitset, $size), "\0") !== '') {
336 1
                throw new InvalidArgumentException('out-of-range bits detected');
337
            }
338 1
            $bitset = \substr($bitset, 0, $size);
339
        }
340
341
        // truncate out-of-range bits of last byte
342 8
        $lastByteMaxOrd = $this->enumerationCount % 8;
343 8
        if ($lastByteMaxOrd !== 0) {
344 8
            $lastByte         = $bitset[-1];
345 8
            $lastByteExpected = \chr((1 << $lastByteMaxOrd) - 1) & $lastByte;
346 8
            if ($lastByte !== $lastByteExpected) {
347 2
                throw new InvalidArgumentException('out-of-range bits detected');
348
            }
349
350 6
            $this->bitset = \substr($bitset, 0, -1) . $lastByteExpected;
351
        }
352
353 6
        $this->bitset = $bitset;
354 6
    }
355
356
    /**
357
     * Set binary bitset in little-endian order
358
     *
359
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
360
     * @return void
361
     * @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...
362
     * @see setBinaryBitsetLeBin()
363
     * @see doSetBinaryBitsetLeBin()
364
     */
365 5
    private function doSetBinaryBitsetLeInt($bitset): void
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $bitset
Loading history...
366
    {
367 5
        $len = \strlen($bitset);
368 5
        $int = 0;
369 5
        for ($i = 0; $i < $len; ++$i) {
370 5
            $ord = \ord($bitset[$i]);
371
372 5
            if ($ord && $i > \PHP_INT_SIZE - 1) {
373 1
                throw new InvalidArgumentException('out-of-range bits detected');
374
            }
375
376 5
            $int |= $ord << (8 * $i);
377
        }
378
379 4
        if ($int & (~0 << $this->enumerationCount)) {
380 2
            throw new InvalidArgumentException('out-of-range bits detected');
381
        }
382
383 2
        $this->bitset = $int;
384 2
    }
385
386
    /**
387
     * Set the given binary bitset in big-endian order
388
     *
389
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
390
     * @return void
391
     * @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...
392
     */
393 1
    public function setBinaryBitsetBe(string $bitset): void
394
    {
395 1
        $this->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset));
396 1
    }
397
398
    /**
399
     * Set a bit at the given ordinal number
400
     *
401
     * @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...
402
     * @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...
403
     * @return void
404
     * @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...
405
     * @uses doSetBitBin()
406
     * @uses doSetBitInt()
407
     * @uses doUnsetBitBin()
408
     * @uses doUnsetBitInt()
409
     */
410 3
    public function setBit(int $ordinal, bool $bit): void
411
    {
412 3
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
413 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...
414
        }
415
416 2
        if ($bit) {
417 2
            $this->{$this->fnDoSetBit}($ordinal);
418
        } else {
419 2
            $this->{$this->fnDoUnsetBit}($ordinal);
420
        }
421 2
    }
422
423
    /**
424
     * Set a bit at the given ordinal number.
425
     *
426
     * This is the binary bitset implementation.
427
     *
428
     * @param int $ordinal Ordinal number of bit to set
429
     * @return void
430
     * @see setBit()
431
     * @see doSetBitInt()
432
     */
433 16
    private function doSetBitBin($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
434
    {
435 16
        $byte = (int) ($ordinal / 8);
436 16
        $this->bitset[$byte] = $this->bitset[$byte] | \chr(1 << ($ordinal % 8));
437 16
    }
438
439
    /**
440
     * Set 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 set
445
     * @return void
446
     * @see setBit()
447
     * @see doSetBitBin()
448
     */
449 65
    private function doSetBitInt($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
450
    {
451 65
        $this->bitset = $this->bitset | (1 << $ordinal);
452 65
    }
453
454
    /**
455
     * Unset a bit at the given ordinal number.
456
     *
457
     * This is the binary bitset implementation.
458
     *
459
     * @param int $ordinal Ordinal number of bit to unset
460
     * @return void
461
     * @see setBit()
462
     * @see doUnsetBitInt()
463
     */
464 12
    private function doUnsetBitBin($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
465
    {
466 12
        $byte = (int) ($ordinal / 8);
467 12
        $this->bitset[$byte] = $this->bitset[$byte] & \chr(~(1 << ($ordinal % 8)));
468 12
    }
469
470
    /**
471
     * Unset a bit at the given ordinal number.
472
     *
473
     * This is the integer bitset implementation.
474
     *
475
     * @param int $ordinal Ordinal number of bit to unset
476
     * @return void
477
     * @see setBit()
478
     * @see doUnsetBitBin()
479
     */
480 24
    private function doUnsetBitInt($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
481
    {
482 24
        $this->bitset = $this->bitset & ~(1 << $ordinal);
483 24
    }
484
485
    /* write access (immutable) */
486
487
    /**
488
     * Creates a new set with the given enumerator object or value added
489
     * @param Enum|null|bool|int|float|string|array $enumerator Enumerator object or value
490
     * @return static
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 27
    public function with($enumerator): self
494
    {
495 27
        $clone = clone $this;
496 27
        $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
497 27
        return $clone;
498
    }
499
500
    /**
501
     * Creates a new set with the given enumeration objects or values added
502
     * @param iterable $enumerators Iterable list of enumerator objects or values
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 5
    public function withIterable(iterable $enumerators): self
507
    {
508 5
        $clone = clone $this;
509 5
        foreach ($enumerators as $enumerator) {
510 5
            $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
511
        }
512 5
        return $clone;
513
    }
514
515
    /**
516
     * Create a new set with the given enumerator object or value removed
517
     * @param Enum|null|bool|int|float|string|array $enumerator Enumerator object or value
518
     * @return static
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 8
    public function without($enumerator): self
522
    {
523 8
        $clone = clone $this;
524 8
        $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
525 8
        return $clone;
526
    }
527
528
    /**
529
     * Creates a new set with the given enumeration objects or values removed
530
     * @param iterable $enumerators Iterable list of enumerator objects or values
531
     * @return static
532
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
533
     */
534 5
    public function withoutIterable(iterable $enumerators): self
535
    {
536 5
        $clone = clone $this;
537 5
        foreach ($enumerators as $enumerator) {
538 5
            $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
539
        }
540 5
        return $clone;
541
    }
542
543
    /**
544
     * Create a new set with enumerators from both this and other (this | other)
545
     *
546
     * @param EnumSet $other EnumSet of the same enumeration to produce the union
547
     * @return static
548
     * @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...
549
     */
550 2
    public function withUnion(EnumSet $other): self
551
    {
552 2
        $clone = clone $this;
553 2
        $clone->setUnion($other);
554 2
        return $clone;
555
    }
556
557
    /**
558
     * Create a new set with enumerators from both this and other (this | other)
559
     *
560
     * @param EnumSet $other EnumSet of the same enumeration to produce the union
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
     * @see withUnion()
564
     * @see setUnion()
565
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
566
     */
567 1
    public function union(EnumSet $other): self
568
    {
569 1
        return $this->withUnion($other);
570
    }
571
572
    /**
573
     * Create a new set with enumerators common to both this and other (this & other)
574
     *
575
     * @param EnumSet $other EnumSet of the same enumeration to produce the intersect
576
     * @return static
577
     * @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...
578
     */
579 2
    public function withIntersect(EnumSet $other): self
580
    {
581 2
        $clone = clone $this;
582 2
        $clone->setIntersect($other);
583 2
        return $clone;
584
    }
585
586
    /**
587
     * Create a new set with enumerators common to both this and other (this & other)
588
     *
589
     * @param EnumSet $other EnumSet of the same enumeration to produce the intersect
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
     * @see withIntersect()
593
     * @see setIntersect()
594
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
595
     */
596 1
    public function intersect(EnumSet $other): self
597
    {
598 1
        return $this->withIntersect($other);
599
    }
600
601
    /**
602
     * Create a new set with enumerators in this but not in other (this - other)
603
     *
604
     * @param EnumSet $other EnumSet of the same enumeration to produce the diff
605
     * @return static
606
     * @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...
607
     */
608 2
    public function withDiff(EnumSet $other): self
609
    {
610 2
        $clone = clone $this;
611 2
        $clone->setDiff($other);
612 2
        return $clone;
613
    }
614
615
    /**
616
     * Create a new set with enumerators in this but not in other (this - other)
617
     *
618
     * @param EnumSet $other EnumSet of the same enumeration to produce the diff
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
     * @see withDiff()
622
     * @see setDiff()
623
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
624
     */
625 1
    public function diff(EnumSet $other): self
626
    {
627 1
        return $this->withDiff($other);
628
    }
629
630
    /**
631
     * Create a new set with enumerators in either this and other but not in both (this ^ other)
632
     *
633
     * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference
634
     * @return static
635
     * @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...
636
     */
637 2
    public function withSymDiff(EnumSet $other): self
638
    {
639 2
        $clone = clone $this;
640 2
        $clone->setSymDiff($other);
641 2
        return $clone;
642
    }
643
644
    /**
645
     * Create a new set with enumerators in either this and other but not in both (this ^ other)
646
     *
647
     * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference
648
     * @return static
649
     * @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...
650
     * @see withSymDiff()
651
     * @see setSymDiff()
652
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
653
     */
654 1
    public function symDiff(EnumSet $other): self
655
    {
656 1
        return $this->withSymDiff($other);
657
    }
658
659
    /**
660
     * Create a new set with the given binary bitset in little-endian order
661
     *
662
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
663
     * @return static
664
     * @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...
665
     * @uses doSetBinaryBitsetLeBin()
666
     * @uses doSetBinaryBitsetLeInt()
667
     */
668 11
    public function withBinaryBitsetLe(string $bitset): self
669
    {
670 11
        $clone = clone $this;
671 11
        $clone->{$this->fnDoSetBinaryBitsetLe}($bitset);
672 5
        return $clone;
673
    }
674
675
    /**
676
     * Create a new set with the given binary bitset in big-endian order
677
     *
678
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
679
     * @return static
680
     * @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...
681
     */
682 1
    public function withBinaryBitsetBe(string $bitset): self
683
    {
684 1
        $clone = $this;
685 1
        $clone->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset));
686 1
        return $clone;
687
    }
688
689
    /**
690
     * Create a new set with the bit at the given ordinal number set
691
     *
692
     * @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...
693
     * @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...
694
     * @return static
695
     * @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...
696
     * @uses doSetBitBin()
697
     * @uses doSetBitInt()
698
     * @uses doUnsetBitBin()
699
     * @uses doUnsetBitInt()
700
     */
701 1
    public function withBit(int $ordinal, bool $bit): self
702
    {
703 1
        $clone = clone $this;
704 1
        $clone->setBit($ordinal, $bit);
705 1
        return $clone;
706
    }
707
708
    /* read access */
709
710
    /**
711
     * Test if the given enumerator exists
712
     * @param Enum|null|bool|int|float|string|array $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
713
     * @return bool
714
     */
715 25
    public function has($enumerator): bool
716
    {
717 25
        return $this->{$this->fnDoGetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
718
    }
719
720
    /**
721
     * Test if the given enumerator exists
722
     * @param Enum|null|bool|int|float|string|array $enumerator
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
723
     * @return bool
724
     * @see has()
725
     * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x
726
     */
727 1
    public function contains($enumerator): bool
728
    {
729 1
        return $this->has($enumerator);
730
    }
731
732
    /* IteratorAggregate */
733
734
    /**
735
     * Get a new iterator
736
     * @return Iterator
737
     * @uses doGetIteratorInt()
738
     * @uses doGetIteratorBin()
739
     */
740 13
    public function getIterator(): Iterator
741
    {
742 13
        return $this->{$this->fnDoGetIterator}();
743
    }
744
745
    /**
746
     * Get a new Iterator.
747
     *
748
     * This is the binary bitset implementation.
749
     *
750
     * @return Iterator
751
     * @see getIterator()
752
     * @see goGetIteratorInt()
753
     */
754 4
    private function doGetIteratorBin()
755
    {
756 4
        $bitset   = $this->bitset;
757 4
        $byteLen  = \strlen($bitset);
758 4
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
759 4
            if ($bitset[$bytePos] === "\0") {
760
                // fast skip null byte
761 4
                continue;
762
            }
763
764 4
            $ord = \ord($bitset[$bytePos]);
765 4
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
766 4
                if ($ord & (1 << $bitPos)) {
767 4
                    $ordinal = $bytePos * 8 + $bitPos;
768 4
                    yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
769
                }
770
            }
771
        }
772 4
    }
773
774
    /**
775
     * Get a new Iterator.
776
     *
777
     * This is the integer bitset implementation.
778
     *
779
     * @return Iterator
780
     * @see getIterator()
781
     * @see doGetIteratorBin()
782
     */
783 9
    private function doGetIteratorInt()
784
    {
785 9
        $count  = $this->enumerationCount;
786 9
        $bitset = $this->bitset;
787 9
        for ($ordinal = 0; $ordinal < $count; ++$ordinal) {
788 9
            if ($bitset & (1 << $ordinal)) {
789 7
                yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
790
            }
791
        }
792 7
    }
793
794
    /* Countable */
795
796
    /**
797
     * Count the number of elements
798
     *
799
     * @return int
800
     * @uses doCountBin()
801
     * @uses doCountInt()
802
     */
803 33
    public function count(): int
804
    {
805 33
        return $this->{$this->fnDoCount}();
806
    }
807
808
    /**
809
     * Count the number of elements.
810
     *
811
     * This is the binary bitset implementation.
812
     *
813
     * @return int
814
     * @see count()
815
     * @see doCountInt()
816
     */
817 15
    private function doCountBin()
818
    {
819 15
        $count   = 0;
820 15
        $bitset  = $this->bitset;
821 15
        $byteLen = \strlen($bitset);
822 15
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
823 15
            if ($bitset[$bytePos] === "\0") {
824
                // fast skip null byte
825 15
                continue;
826
            }
827
828 15
            $ord = \ord($bitset[$bytePos]);
829 15
            if ($ord & 0b00000001) ++$count;
830 15
            if ($ord & 0b00000010) ++$count;
0 ignored issues
show
Coding Style Best Practice introduced by
It is generally a best practice to always use braces with control structures.

Adding braces to control structures avoids accidental mistakes as your code changes:

// Without braces (not recommended)
if (true)
    doSomething();

// Recommended
if (true) {
    doSomething();
}
Loading history...
831 15
            if ($ord & 0b00000100) ++$count;
0 ignored issues
show
Coding Style Best Practice introduced by
It is generally a best practice to always use braces with control structures.

Adding braces to control structures avoids accidental mistakes as your code changes:

// Without braces (not recommended)
if (true)
    doSomething();

// Recommended
if (true) {
    doSomething();
}
Loading history...
832 15
            if ($ord & 0b00001000) ++$count;
0 ignored issues
show
Coding Style Best Practice introduced by
It is generally a best practice to always use braces with control structures.

Adding braces to control structures avoids accidental mistakes as your code changes:

// Without braces (not recommended)
if (true)
    doSomething();

// Recommended
if (true) {
    doSomething();
}
Loading history...
833 15
            if ($ord & 0b00010000) ++$count;
834 15
            if ($ord & 0b00100000) ++$count;
835 15
            if ($ord & 0b01000000) ++$count;
836 15
            if ($ord & 0b10000000) ++$count;
837
        }
838 15
        return $count;
839
    }
840
841
    /**
842
     * Count the number of elements.
843
     *
844
     * This is the integer bitset implementation.
845
     *
846
     * @return int
847
     * @see count()
848
     * @see doCountBin()
849
     */
850 18
    private function doCountInt()
851
    {
852 18
        $count  = 0;
853 18
        $bitset = $this->bitset;
854
855
        // PHP does not support right shift unsigned
856 18
        if ($bitset < 0) {
857 5
            $count  = 1;
858 5
            $bitset = $bitset & \PHP_INT_MAX;
859
        }
860
861
        // iterate byte by byte and count set bits
862 18
        $phpIntBitSize = \PHP_INT_SIZE * 8;
863 18
        for ($bitPos = 0; $bitPos < $phpIntBitSize; $bitPos += 8) {
864 18
            $bitChk = 0xff << $bitPos;
865 18
            $byte = $bitset & $bitChk;
866 18
            if ($byte) {
867 17
                $byte = $byte >> $bitPos;
868 17
                if ($byte & 0b00000001) ++$count;
869 17
                if ($byte & 0b00000010) ++$count;
870 17
                if ($byte & 0b00000100) ++$count;
871 17
                if ($byte & 0b00001000) ++$count;
872 17
                if ($byte & 0b00010000) ++$count;
873 17
                if ($byte & 0b00100000) ++$count;
874 17
                if ($byte & 0b01000000) ++$count;
875 17
                if ($byte & 0b10000000) ++$count;
876
            }
877
878 18
            if ($bitset <= $bitChk) {
879 18
                break;
880
            }
881
        }
882
883 18
        return $count;
884
    }
885
886
    /**
887
     * Check if this EnumSet is the same as other
888
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
889
     * @return bool
890
     */
891 3
    public function isEqual(EnumSet $other): bool
892
    {
893 3
        return $this->enumeration === $other->enumeration
894 3
            && $this->bitset === $other->bitset;
895
    }
896
897
    /**
898
     * Check if this EnumSet is a subset of other
899
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
900
     * @return bool
901
     */
902 4
    public function isSubset(EnumSet $other): bool
903
    {
904 4
        return $this->enumeration === $other->enumeration
905 4
            && ($this->bitset & $other->bitset) === $this->bitset;
906
    }
907
908
    /**
909
     * Check if this EnumSet is a superset of other
910
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
911
     * @return bool
912
     */
913 4
    public function isSuperset(EnumSet $other): bool
914
    {
915 4
        return $this->enumeration === $other->enumeration
916 4
            && ($this->bitset | $other->bitset) === $this->bitset;
917
    }
918
919
    /**
920
     * Tests if the set is empty
921
     *
922
     * @return bool
923
     */
924 5
    public function isEmpty(): bool
925
    {
926 5
        return $this->bitset === $this->emptyBitset;
927
    }
928
929
    /**
930
     * Get ordinal numbers of the defined enumerators as array
931
     * @return int[]
932
     * @uses  doGetOrdinalsBin()
933
     * @uses  doGetOrdinalsInt()
934
     */
935 19
    public function getOrdinals(): array
936
    {
937 19
        return $this->{$this->fnDoGetOrdinals}();
938
    }
939
940
    /**
941
     * Get ordinal numbers of the defined enumerators as array.
942
     *
943
     * This is the binary bitset implementation.
944
     *
945
     * @return int[]
946
     * @see getOrdinals()
947
     * @see goGetOrdinalsInt()
948
     */
949 1
    private function doGetOrdinalsBin()
950
    {
951 1
        $ordinals = [];
952 1
        $bitset   = $this->bitset;
953 1
        $byteLen  = \strlen($bitset);
954 1
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
955 1
            if ($bitset[$bytePos] === "\0") {
956
                // fast skip null byte
957 1
                continue;
958
            }
959
960 1
            $ord = \ord($bitset[$bytePos]);
961 1
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
962 1
                if ($ord & (1 << $bitPos)) {
963 1
                    $ordinals[] = $bytePos * 8 + $bitPos;
964
                }
965
            }
966
        }
967 1
        return $ordinals;
968
    }
969
970
    /**
971
     * Get ordinal numbers of the defined enumerators as array.
972
     *
973
     * This is the integer bitset implementation.
974
     *
975
     * @return int[]
976
     * @see getOrdinals()
977
     * @see doGetOrdinalsBin()
978
     */
979 18
    private function doGetOrdinalsInt()
980
    {
981 18
        $ordinals = [];
982 18
        $count    = $this->enumerationCount;
983 18
        $bitset   = $this->bitset;
984 18
        for ($ordinal = 0; $ordinal < $count; ++$ordinal) {
985 18
            if ($bitset & (1 << $ordinal)) {
986 18
                $ordinals[] = $ordinal;
987
            }
988
        }
989 18
        return $ordinals;
990
    }
991
992
    /**
993
     * Get values of the defined enumerators as array
994
     * @return mixed[]
995
     */
996 14
    public function getValues(): array
997
    {
998 14
        $enumeration = $this->enumeration;
999 14
        $values      = [];
1000 14
        foreach ($this->getOrdinals() as $ord) {
1001 14
            $values[] = $enumeration::byOrdinal($ord)->getValue();
1002
        }
1003 14
        return $values;
1004
    }
1005
1006
    /**
1007
     * Get names of the defined enumerators as array
1008
     * @return string[]
1009
     */
1010 1
    public function getNames(): array
1011
    {
1012 1
        $enumeration = $this->enumeration;
1013 1
        $names       = [];
1014 1
        foreach ($this->getOrdinals() as $ord) {
1015 1
            $names[] = $enumeration::byOrdinal($ord)->getName();
1016
        }
1017 1
        return $names;
1018
    }
1019
1020
    /**
1021
     * Get the defined enumerators as array
1022
     * @return Enum[]
1023
     */
1024 2
    public function getEnumerators(): array
1025
    {
1026 2
        $enumeration = $this->enumeration;
1027 2
        $enumerators = [];
1028 2
        foreach ($this->getOrdinals() as $ord) {
1029 2
            $enumerators[] = $enumeration::byOrdinal($ord);
1030
        }
1031 2
        return $enumerators;
1032
    }
1033
1034
    /**
1035
     * Get binary bitset in little-endian order
1036
     *
1037
     * @return string
1038
     * @uses doGetBinaryBitsetLeBin()
1039
     * @uses doGetBinaryBitsetLeInt()
1040
     */
1041 8
    public function getBinaryBitsetLe(): string
1042
    {
1043 8
        return $this->{$this->fnDoGetBinaryBitsetLe}();
1044
    }
1045
1046
    /**
1047
     * Get binary bitset in little-endian order.
1048
     *
1049
     * This is the binary bitset implementation.
1050
     *
1051
     * @return string
1052
     * @see getBinaryBitsetLe()
1053
     * @see doGetBinaryBitsetLeInt()
1054
     */
1055 5
    private function doGetBinaryBitsetLeBin()
1056
    {
1057 5
        return $this->bitset;
1058
    }
1059
1060
    /**
1061
     * Get binary bitset in little-endian order.
1062
     *
1063
     * This is the integer bitset implementation.
1064
     *
1065
     * @return string
1066
     * @see getBinaryBitsetLe()
1067
     * @see doGetBinaryBitsetLeBin()
1068
     */
1069 3
    private function doGetBinaryBitsetLeInt()
1070
    {
1071 3
        $bin = \pack(\PHP_INT_SIZE === 8 ? 'P' : 'V', $this->bitset);
1072 3
        return \substr($bin, 0, (int)\ceil($this->enumerationCount / 8));
1073
    }
1074
1075
    /**
1076
     * Get binary bitset in big-endian order
1077
     *
1078
     * @return string
1079
     */
1080 2
    public function getBinaryBitsetBe(): string
1081
    {
1082 2
        return \strrev($this->getBinaryBitsetLe());
1083
    }
1084
1085
    /**
1086
     * Get a bit at the given ordinal number
1087
     *
1088
     * @param int $ordinal Ordinal number of bit to get
1089
     * @return bool
1090
     * @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...
1091
     * @uses doGetBitBin()
1092
     * @uses doGetBitInt()
1093
     */
1094 4
    public function getBit(int $ordinal): bool
1095
    {
1096 4
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
1097 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...
1098
        }
1099
1100 3
        return $this->{$this->fnDoGetBit}($ordinal);
1101
    }
1102
1103
    /**
1104
     * Get a bit at the given ordinal number.
1105
     *
1106
     * This is the binary bitset implementation.
1107
     *
1108
     * @param int $ordinal Ordinal number of bit to get
1109
     * @return bool
1110
     * @see getBit()
1111
     * @see doGetBitInt()
1112
     */
1113 8
    private function doGetBitBin($ordinal)
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
1114
    {
1115 8
        return (\ord($this->bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
1116
    }
1117
1118
    /**
1119
     * Get a bit at the given ordinal number.
1120
     *
1121
     * This is the integer bitset implementation.
1122
     *
1123
     * @param int $ordinal Ordinal number of bit to get
1124
     * @return bool
1125
     * @see getBit()
1126
     * @see doGetBitBin()
1127
     */
1128 19
    private function doGetBitInt($ordinal)
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
1129
    {
1130 19
        return (bool)($this->bitset & (1 << $ordinal));
1131
    }
1132
}
1133