Completed
Push — immutable-set ( 56a84d )
by Marc
03:51
created

EnumSet::withoutEnumerator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MabeEnum;
6
7
use Countable;
8
use InvalidArgumentException;
9
use Iterator;
10
use IteratorAggregate;
11
use OutOfBoundsException;
12
13
/**
14
 * A set of enumerators of the given enumeration (EnumSet<T>)
15
 * based on an integer or binary bitset depending of given enumeration size.
16
 *
17
 * @copyright 2019 Marc Bennewitz
18
 * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
19
 * @link http://github.com/marc-mabe/php-enum for the canonical source repository
20
 */
3 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @package tag in class comment
Loading history...
Coding Style introduced by
Missing @author tag in class comment
Loading history...
21
class EnumSet implements IteratorAggregate, Countable
22
{
23
    /**
24
     * The classname of the Enumeration
25
     * @var string
26
     */
27
    private $enumeration;
1 ignored issue
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
Coding Style introduced by
Private member variable "enumeration" must contain a leading underscore
Loading history...
28
29
    /**
30
     * Number of enumerators defined in the enumeration
31
     * @var int
32
     */
33
    private $enumerationCount;
1 ignored issue
show
Coding Style introduced by
Private member variable "enumerationCount" must contain a leading underscore
Loading history...
34
35
    /**
36
     * Integer or binary (little endian) bitset
37
     * @var int|string
38
     */
39
    private $bitset = 0;
1 ignored issue
show
Coding Style introduced by
Private member variable "bitset" must contain a leading underscore
Loading history...
40
41
    /**#@+
42
     * Defines private method names to be called depended of how the bitset type was set too.
43
     * ... Integer or binary bitset.
44
     * ... *Int or *Bin method
45
     * 
46
     * @var string
47
     */
48
    private $fnDoCount             = 'doCountInt';
1 ignored issue
show
Coding Style introduced by
Private member variable "fnDoCount" must contain a leading underscore
Loading history...
49
    private $fnDoGetOrdinals       = 'doGetOrdinalsInt';
1 ignored issue
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
Coding Style introduced by
Private member variable "fnDoGetOrdinals" must contain a leading underscore
Loading history...
50
    private $fnDoGetBit            = 'doGetBitInt';
1 ignored issue
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
Coding Style introduced by
Private member variable "fnDoGetBit" must contain a leading underscore
Loading history...
51
    private $fnDoSetBit            = 'doSetBitInt';
1 ignored issue
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
Coding Style introduced by
Private member variable "fnDoSetBit" must contain a leading underscore
Loading history...
52
    private $fnDoUnsetBit          = 'doUnsetBitInt';
1 ignored issue
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
Coding Style introduced by
Private member variable "fnDoUnsetBit" must contain a leading underscore
Loading history...
53
    private $fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeInt';
1 ignored issue
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
Coding Style introduced by
Private member variable "fnDoGetBinaryBitsetLe" must contain a leading underscore
Loading history...
54
    private $fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeInt';
1 ignored issue
show
Coding Style introduced by
Expected 1 blank line before member var; 0 found
Loading history...
Coding Style introduced by
Private member variable "fnDoSetBinaryBitsetLe" must contain a leading underscore
Loading history...
55
    /**#@-*/
56
57
    /**
58
     * Constructor
59
     *
60
     * @param string $enumeration The classname of the enumeration
1 ignored issue
show
introduced by
Parameter comment must end with a full stop
Loading history...
61
     * @throws InvalidArgumentException
0 ignored issues
show
introduced by
Comment missing for @throws tag in function comment
Loading history...
62
     */
63 80
    public function __construct(string $enumeration)
64
    {
65 80
        if (!\is_subclass_of($enumeration, Enum::class)) {
66 1
            throw new InvalidArgumentException(\sprintf(
67 1
                '%s can handle subclasses of %s only',
68 1
                __METHOD__,
69 1
                Enum::class
70
            ));
71
        }
72
73 79
        $this->enumeration      = $enumeration;
74 79
        $this->enumerationCount = \count($enumeration::getConstants());
75
76
        // By default the bitset is initialized as integer bitset
77
        // in case the enumeraton has more enumerators then integer bits
78
        // we will switch this into a binary bitset
79 79
        if ($this->enumerationCount > \PHP_INT_SIZE * 8) {
80
            // init binary bitset with zeros
81 21
            $this->bitset = \str_repeat("\0", (int)\ceil($this->enumerationCount / 8));
82
83
            // switch internal binary bitset functions
84 21
            $this->fnDoCount             = 'doCountBin';
85 21
            $this->fnDoGetOrdinals       = 'doGetOrdinalsBin';
86 21
            $this->fnDoGetBit            = 'doGetBitBin';
87 21
            $this->fnDoSetBit            = 'doSetBitBin';
88 21
            $this->fnDoUnsetBit          = 'doUnsetBitBin';
89 21
            $this->fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeBin';
90 21
            $this->fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeBin';
91
        }
92 79
    }
93
94
    /**
95
     * Get the classname of the enumeration
96
     * @return string
97
     */
98 1
    public function getEnumeration(): string
99
    {
100 1
        return $this->enumeration;
101
    }
102
103
    /**
104
     * Adds the given enumerator
105
     * @param Enum|null|bool|int|float|string|array $enumerator
1 ignored issue
show
Documentation introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected "Enum|null|boolean|integer|float|string|array" but found "Enum|null|bool|int|float|string|array" for parameter type
Loading history...
106
     * @return static
107
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
108
     */
109 50
    public function withEnumerator($enumerator): self
110
    {
111 50
        $clone = clone $this;
112 50
        $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
113 50
        return $clone;
114
    }
115
116
    /**
117
     * Removed an enumerator
118
     * @param Enum|null|bool|int|float|string|array $enumerator
1 ignored issue
show
Documentation introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected "Enum|null|boolean|integer|float|string|array" but found "Enum|null|bool|int|float|string|array" for parameter type
Loading history...
119
     * @return static
120
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
121
     */
122 4
    public function withoutEnumerator($enumerator): self
123
    {
124 4
        $clone = clone $this;
125 4
        $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
126 4
        return $clone;
127
    }
128
129
    /**
130
     * Test if the given enumerator exists
131
     * @param Enum|null|bool|int|float|string|array $enumerator
1 ignored issue
show
Documentation introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected "Enum|null|boolean|integer|float|string|array" but found "Enum|null|bool|int|float|string|array" for parameter type
Loading history...
132
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
133
     */
134 11
    public function contains($enumerator): bool
135
    {
136 11
        return $this->{$this->fnDoGetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
137
    }
138
139
    /* IteratorAggregate */
140
141
    /**
142
     * Create and return a new iterator
143
     * @return Iterator
144
     */
145
    public function getIterator(): Iterator
146
    {
147
        return new class($this, $this->enumeration, $this->enumerationCount, $this->bitset) implements Iterator {
1 ignored issue
show
Coding Style introduced by
This line exceeds maximum limit of 100 characters; contains 113 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
148
            /**
149
             * @var EnumSet
150
             */
151
            private $enumSet;
152
153
            /**
154
             * @var string
155
             */
156
            private $enumeration;
157
158
            /**
159
             * @var int
160
             */
161
            private $enumerationCount;
162
163
            /**
164
             * @var int|string
165
             */
166
            private $bitset;
167
168
            /**
169
             * @var int
170
             */
171
            private $ordinal = -1;
172
173
            /**
174
             * @var string
175
             */
176
            private $fnDoRewind = 'doRewindInt';
177
178
            /**
179
             * Constructor.
180
             * @param EnumSet    $enumSet
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
181
             * @param string     $enumeration
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
182
             * @param int        $enumerationCount
1 ignored issue
show
Documentation introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
183
             * @param int|string $bitset
1 ignored issue
show
Documentation introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected "integer|string" but found "int|string" for parameter type
Loading history...
184
             */
185 32
            public function __construct(EnumSet $enumSet, $enumeration, $enumerationCount, $bitset)
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $enumeration
Loading history...
Coding Style introduced by
Type hint "int" missing for $enumerationCount
Loading history...
186
            {
187 32
                $this->enumSet          = $enumSet;
188 32
                $this->enumeration      = $enumeration;
189 32
                $this->enumerationCount = $enumerationCount;
190 32
                $this->bitset           = $bitset;
191
192
                // By default the bitset is initialized as integer bitset
193
                // in case the enumeraton has more enumerators then integer bits
194
                // we will switch this into a binary bitset
195 32
                if ($this->enumerationCount > \PHP_INT_SIZE * 8) {
196 11
                    $this->fnDoRewind = 'doRewindBin';
197
                }
198
199
                // go to the first valid iterator position (if any)
200 32
                $this->next();
201 32
            }
202
203
            /**
204
             * Get the current enumerator
205
             * @return Enum Returns the current enumerator object
206
             * @throws OutOfBoundsException On an invalid iterator position
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
207
             */
208 30
            public function current(): Enum
209
            {
210 30
                if (!$this->valid()) {
211 11
                    throw new OutOfBoundsException('Invalid iterator position');
212
                }
213
214 24
                return ($this->enumeration)::byOrdinal($this->ordinal);
215
            }
216
217
            /**
218
             * Get the ordinal number of the current iterator position
219
             * @return int The ordinal number of the current enumerator
1 ignored issue
show
Coding Style introduced by
Expected "integer" but found "int" for function return type
Loading history...
220
             * @throws OutOfBoundsException On an invalid iterator position
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
221
             */
222 23
            public function key(): int
223
            {
224 23
                if (!$this->valid()) {
225 6
                    throw new OutOfBoundsException('Invalid iterator position');
226
                }
227
228 22
                return $this->ordinal;
229
            }
230
231
            /**
232
             * Go to the next valid iterator position.
233
             * If no valid iterator position is found the iterator position will be the last possible + 1.
1 ignored issue
show
Coding Style introduced by
This line exceeds maximum limit of 100 characters; contains 106 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
234
             * @return void
235
             */
236 32
            public function next(): void
237
            {
238
                do {
239 32
                    if (++$this->ordinal >= $this->enumerationCount) {
240 31
                        $this->ordinal = $this->enumerationCount;
241 31
                        return;
242
                    }
243 32
                } while (!$this->enumSet->getBit($this->ordinal));
244 24
            }
245
246
            /**
247
             * Go to the first valid iterator position.
248
             * If no valid iterator position was found the iterator position will be 0.
249
             * @return void
250
             * @uses doRewindBin()
251
             * @uses doRewindInt()
252
             */
253 19
            public function rewind(): void
254
            {
255 19
                $this->{$this->fnDoRewind}();
256 19
            }
257
258
            /**
259
             * Go to the first valid iterator position.
260
             * If no valid iterator position was found the iterator position will be 0.
261
             *
262
             * This is the binary bitset implementation.
263
             *
264
             * @return void
265
             * @see rewind()
266
             * @see doRewindInt()
267
             */
268 7
            private function doRewindBin(): void
269
            {
270 7
                if (\ltrim($this->bitset, "\0") !== '') {
271 5
                    $this->ordinal = -1;
272 5
                    $this->next();
273
                } else {
274 2
                    $this->ordinal = 0;
275
                }
276 7
            }
277
278
            /**
279
             * Go to the first valid iterator position.
280
             * If no valid iterator position was found the iterator position will be 0.
281
             *
282
             * This is the integer bitset implementation.
283
             *
284
             * @return void
285
             * @see rewind()
286
             * @see doRewindBin()
287
             */
288 12
            private function doRewindInt(): void
289
            {
290 12
                if ($this->bitset) {
291 8
                    $this->ordinal = -1;
292 8
                    $this->next();
293
                } else {
294 4
                    $this->ordinal = 0;
295
                }
296 12
            }
297
298
            /**
299
             * Test if the current iterator position is valid
300
             * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
301
             */
302 32
            public function valid(): bool
303
            {
304 32
                return $this->ordinal !== $this->enumerationCount && $this->enumSet->getBit($this->ordinal);
1 ignored issue
show
Coding Style introduced by
This line exceeds maximum limit of 100 characters; contains 108 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
305
            }
306
        };
307
    }
308
309
    /* Countable */
310
311
    /**
312
     * Count the number of elements
313
     *
314
     * @return int
1 ignored issue
show
Coding Style introduced by
Expected "integer" but found "int" for function return type
Loading history...
315
     * @uses doCountBin()
316
     * @uses doCountInt()
317
     */
318 15
    public function count(): int
319
    {
320 15
        return $this->{$this->fnDoCount}();
321
    }
322
323
    /**
324
     * Count the number of elements.
325
     *
326
     * This is the binary bitset implementation.
327
     *
328
     * @return int
1 ignored issue
show
Coding Style introduced by
Expected "integer" but found "int" for function return type
Loading history...
329
     * @see count()
330
     * @see doCountInt()
331
     */
332 7
    private function doCountBin(): int
333
    {
334 7
        $count   = 0;
335 7
        $bitset  = $this->bitset;
336 7
        $byteLen = \strlen($bitset);
337 7
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
338 7
            if ($bitset[$bytePos] === "\0") {
339
                // fast skip null byte
340 5
                continue;
341
            }
342
343 7
            $ord = \ord($bitset[$bytePos]);
344 7
            if ($ord & 0b00000001) ++$count;
1 ignored issue
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...
345 7
            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...
346 7
            if ($ord & 0b00000100) ++$count;
1 ignored issue
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...
347 7
            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...
348 7
            if ($ord & 0b00010000) ++$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...
349 7
            if ($ord & 0b00100000) ++$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...
350 7
            if ($ord & 0b01000000) ++$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...
351 7
            if ($ord & 0b10000000) ++$count;
1 ignored issue
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...
352
        }
353 7
        return $count;
354
    }
355
356
    /**
357
     * Count the number of elements.
358
     *
359
     * This is the integer bitset implementation.
360
     *
361
     * @return int
1 ignored issue
show
Coding Style introduced by
Expected "integer" but found "int" for function return type
Loading history...
362
     * @see count()
363
     * @see doCountBin()
364
     */
365 8
    private function doCountInt(): int
366
    {
367 8
        $count  = 0;
368 8
        $bitset = $this->bitset;
369
370
        // PHP does not support right shift unsigned
371 8
        if ($bitset < 0) {
372 2
            $count  = 1;
373 2
            $bitset = $bitset & \PHP_INT_MAX;
374
        }
375
376
        // iterate byte by byte and count set bits
377 8
        $phpIntBitSize = \PHP_INT_SIZE * 8;
378 8
        for ($bitPos = 0; $bitPos < $phpIntBitSize; $bitPos += 8) {
379 8
            $bitChk = 0xff << $bitPos;
380 8
            $byte = $bitset & $bitChk;
381 8
            if ($byte) {
382 7
                $byte = $byte >> $bitPos;
383 7
                if ($byte & 0b00000001) ++$count;
1 ignored issue
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...
384 7
                if ($byte & 0b00000010) ++$count;
1 ignored issue
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...
385 7
                if ($byte & 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...
386 7
                if ($byte & 0b00001000) ++$count;
1 ignored issue
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...
387 7
                if ($byte & 0b00010000) ++$count;
1 ignored issue
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...
388 7
                if ($byte & 0b00100000) ++$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...
389 7
                if ($byte & 0b01000000) ++$count;
1 ignored issue
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...
390 7
                if ($byte & 0b10000000) ++$count;
1 ignored issue
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...
391
            }
392
393 8
            if ($bitset <= $bitChk) {
394 7
                break;
395
            }
396
        }
397
398 8
        return $count;
399
    }
400
401
    /**
402
     * Check if this EnumSet is the same as other
403
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
404
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
405
     */
406 3
    public function isEqual(EnumSet $other): bool
407
    {
408 3
        return $this->enumeration === $other->enumeration
409 3
            && $this->bitset === $other->bitset;
410
    }
411
412
    /**
413
     * Check if this EnumSet is a subset of other
414
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
415
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
416
     */
417 4
    public function isSubset(EnumSet $other): bool
418
    {
419 4
        return $this->enumeration === $other->enumeration
420 4
            && ($this->bitset & $other->bitset) === $this->bitset;
421
    }
422
423
    /**
424
     * Check if this EnumSet is a superset of other
425
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
426
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
427
     */
428 4
    public function isSuperset(EnumSet $other): bool
429
    {
430 4
        return $this->enumeration === $other->enumeration
431 4
            && ($this->bitset | $other->bitset) === $this->bitset;
432
    }
433
434
    /**
435
     * Produce a new set with enumerators from both this and other (this | other)
436
     *
437
     * @param EnumSet $other EnumSet of the same enumeration to produce the union
1 ignored issue
show
introduced by
Parameter comment must end with a full stop
Loading history...
438
     * @return EnumSet
439
     * @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...
440
     */
441 2
    public function union(EnumSet $other): EnumSet
442
    {
443 2
        if ($this->enumeration !== $other->enumeration) {
444 1
            throw new InvalidArgumentException(\sprintf(
445 1
                'Other should be of the same enumeration as this %s',
446 1
                $this->enumeration
447
            ));
448
        }
449
450 1
        $clone = clone $this;
451 1
        $clone->bitset = $this->bitset | $other->bitset;
452 1
        return $clone;
453
    }
454
455
    /**
456
     * Produce a new set with enumerators common to both this and other (this & other)
457
     *
458
     * @param EnumSet $other EnumSet of the same enumeration to produce the intersect
1 ignored issue
show
introduced by
Parameter comment must end with a full stop
Loading history...
459
     * @return EnumSet
460
     * @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...
461
     */
462 2
    public function intersect(EnumSet $other): EnumSet
463
    {
464 2
        if ($this->enumeration !== $other->enumeration) {
465 1
            throw new InvalidArgumentException(\sprintf(
466 1
                'Other should be of the same enumeration as this %s',
467 1
                $this->enumeration
468
            ));
469
        }
470
471 1
        $clone = clone $this;
472 1
        $clone->bitset = $this->bitset & $other->bitset;
473 1
        return $clone;
474
    }
475
476
    /**
477
     * Produce a new set with enumerators in this but not in other (this - other)
478
     *
479
     * @param EnumSet $other EnumSet of the same enumeration to produce the diff
1 ignored issue
show
introduced by
Parameter comment must end with a full stop
Loading history...
480
     * @return EnumSet
481
     * @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...
482
     */
483 2
    public function diff(EnumSet $other): EnumSet
484
    {
485 2
        if ($this->enumeration !== $other->enumeration) {
486 1
            throw new InvalidArgumentException(\sprintf(
487 1
                'Other should be of the same enumeration as this %s',
488 1
                $this->enumeration
489
            ));
490
        }
491
492 1
        $clone = clone $this;
493 1
        $clone->bitset = $this->bitset & ~$other->bitset;
494 1
        return $clone;
495
    }
496
497
    /**
498
     * Produce a new set with enumerators in either this and other but not in both (this ^ other)
499
     *
500
     * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference
1 ignored issue
show
introduced by
Parameter comment must end with a full stop
Loading history...
501
     * @return EnumSet
502
     * @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...
503
     */
504 2
    public function symDiff(EnumSet $other): EnumSet
505
    {
506 2
        if ($this->enumeration !== $other->enumeration) {
507 1
            throw new InvalidArgumentException(\sprintf(
508 1
                'Other should be of the same enumeration as this %s',
509 1
                $this->enumeration
510
            ));
511
        }
512
513 1
        $clone = clone $this;
514 1
        $clone->bitset = $this->bitset ^ $other->bitset;
515 1
        return $clone;
516
    }
517
518
    /**
519
     * Get ordinal numbers of the defined enumerators as array
520
     * @return int[]
521
     * @uses  doGetOrdinalsBin()
522
     * @uses  doGetOrdinalsInt()
523
     */
524 9
    public function getOrdinals(): array
525
    {
526 9
        return $this->{$this->fnDoGetOrdinals}();
527
    }
528
529
    /**
530
     * Get ordinal numbers of the defined enumerators as array.
531
     *
532
     * This is the binary bitset implementation.
533
     *
534
     * @return int[]
535
     * @see getOrdinals()
536
     * @see goGetOrdinalsInt()
537
     */
538 1
    private function doGetOrdinalsBin(): array
539
    {
540 1
        $ordinals = [];
541 1
        $bitset   = $this->bitset;
542 1
        $byteLen  = \strlen($bitset);
543 1
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
544 1
            if ($bitset[$bytePos] === "\0") {
545
                // fast skip null byte
546 1
                continue;
547
            }
548
549 1
            $ord = \ord($bitset[$bytePos]);
550 1
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
551 1
                if ($ord & (1 << $bitPos)) {
552 1
                    $ordinals[] = $bytePos * 8 + $bitPos;
553
                }
554
            }
555
        }
556 1
        return $ordinals;
557
    }
558
559
    /**
560
     * Get ordinal numbers of the defined enumerators as array.
561
     *
562
     * This is the integer bitset implementation.
563
     *
564
     * @return int[]
565
     * @see getOrdinals()
566
     * @see doGetOrdinalsBin()
567
     */
568 8
    private function doGetOrdinalsInt(): array
569
    {
570 8
        $ordinals   = [];
571 8
        $ordinalMax = $this->enumerationCount;
572 8
        $bitset     = $this->bitset;
573 8
        for ($ord = 0; $ord < $ordinalMax; ++$ord) {
574 8
            if ($bitset & (1 << $ord)) {
575 8
                $ordinals[] = $ord;
576
            }
577
        }
578 8
        return $ordinals;
579
    }
580
581
    /**
582
     * Get values of the defined enumerators as array
583
     * @return mixed[]
584
     */
585 5
    public function getValues(): array
586
    {
587 5
        $enumeration = $this->enumeration;
588 5
        $values      = [];
589 5
        foreach ($this->getOrdinals() as $ord) {
590 5
            $values[] = $enumeration::byOrdinal($ord)->getValue();
591
        }
592 5
        return $values;
593
    }
594
595
    /**
596
     * Get names of the defined enumerators as array
597
     * @return string[]
598
     */
599 1
    public function getNames(): array
600
    {
601 1
        $enumeration = $this->enumeration;
602 1
        $names       = [];
603 1
        foreach ($this->getOrdinals() as $ord) {
604 1
            $names[] = $enumeration::byOrdinal($ord)->getName();
605
        }
606 1
        return $names;
607
    }
608
609
    /**
610
     * Get the defined enumerators as array
611
     * @return Enum[]
612
     */
613 1
    public function getEnumerators(): array
614
    {
615 1
        $enumeration = $this->enumeration;
616 1
        $enumerators = [];
617 1
        foreach ($this->getOrdinals() as $ord) {
618 1
            $enumerators[] = $enumeration::byOrdinal($ord);
619
        }
620 1
        return $enumerators;
621
    }
622
623
    /**
624
     * Get binary bitset in little-endian order
625
     * 
626
     * @return string
627
     * @uses doGetBinaryBitsetLeBin()
628
     * @uses doGetBinaryBitsetLeInt()
629
     */
630 6
    public function getBinaryBitsetLe(): string
631
    {
632 6
        return $this->{$this->fnDoGetBinaryBitsetLe}();
633
    }
634
635
    /**
636
     * Get binary bitset in little-endian order.
637
     *
638
     * This is the binary bitset implementation.
639
     *
640
     * @return string
641
     * @see getBinaryBitsetLe()
642
     * @see doGetBinaryBitsetLeInt()
643
     */
644 4
    private function doGetBinaryBitsetLeBin(): string
645
    {
646 4
        return $this->bitset;
647
    }
648
649
    /**
650
     * Get binary bitset in little-endian order.
651
     *
652
     * This is the integer bitset implementation.
653
     *
654
     * @return string
655
     * @see getBinaryBitsetLe()
656
     * @see doGetBinaryBitsetLeBin()
657
     */
658 2
    private function doGetBinaryBitsetLeInt(): string
659
    {
660 2
        $bin = \pack(\PHP_INT_SIZE === 8 ? 'P' : 'V', $this->bitset);
661 2
        return \substr($bin, 0, (int)\ceil($this->enumerationCount / 8));
662
    }
663
664
    /**
665
     * Set binary bitset in little-endian order
666
     *
667
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
668
     * @return static
0 ignored issues
show
Documentation Bug introduced by
The doc comment static at position 0 could not be parsed: 'static' is only available from within classes.
Loading history...
669
     * @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...
670
     * @uses doSetBinaryBitsetLeBin()
671
     * @uses doSetBinaryBitsetLeInt()
672
     */
673 12
    public function withBinaryBitsetLe(string $bitset): self
674
    {
675 12
        $clone = clone $this;
676 12
        $clone->{$this->fnDoSetBinaryBitsetLe}($bitset);
677 6
        return $clone;
678
    }
679
680
    /**
681
     * Set binary bitset in little-endian order
682
     *
683
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
684
     * @return void
685
     * @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...
686
     * @see setBinaryBitsetLeBin()
687
     * @see doSetBinaryBitsetLeInt()
688
     */
689 7
    private function doSetBinaryBitsetLeBin(string $bitset): void
690
    {
691 7
        $size   = \strlen($this->bitset);
692 7
        $sizeIn = \strlen($bitset);
693
694 7
        if ($sizeIn < $size) {
695
            // add "\0" if the given bitset is not long enough
696 1
            $bitset .= \str_repeat("\0", $size - $sizeIn);
697 6
        } elseif ($sizeIn > $size) {
698 2
            if (\ltrim(\substr($bitset, $size), "\0") !== '') {
699 1
                throw new InvalidArgumentException('out-of-range bits detected');
700
            }
701 1
            $bitset = \substr($bitset, 0, $size);
702
        }
703
704
        // truncate out-of-range bits of last byte
705 6
        $lastByteMaxOrd = $this->enumerationCount % 8;
706 6
        if ($lastByteMaxOrd !== 0) {
707 6
            $lastByte         = $bitset[-1];
708 6
            $lastByteExpected = \chr((1 << $lastByteMaxOrd) - 1) & $lastByte;
709 6
            if ($lastByte !== $lastByteExpected) {
710 2
                throw new InvalidArgumentException('out-of-range bits detected');
711
            }
712
713 4
            $this->bitset = \substr($bitset, 0, -1) . $lastByteExpected;
714
        }
715
716 4
        $this->bitset = $bitset;
717 4
    }
718
719
    /**
720
     * Set binary bitset in little-endian order
721
     *
722
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
723
     * @return void
724
     * @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...
725
     * @see setBinaryBitsetLeBin()
726
     * @see doSetBinaryBitsetLeBin()
727
     */
728 5
    private function doSetBinaryBitsetLeInt(string $bitset): void
729
    {
730 5
        $len = \strlen($bitset);
731 5
        $int = 0;
732 5
        for ($i = 0; $i < $len; ++$i) {
733 5
            $ord = \ord($bitset[$i]);
734
735 5
            if ($ord && $i > \PHP_INT_SIZE - 1) {
736 1
                throw new InvalidArgumentException('out-of-range bits detected');
737
            }
738
739 5
            $int |= $ord << (8 * $i);
740
        }
741
742 4
        if ($int & (~0 << $this->enumerationCount)) {
743 2
            throw new InvalidArgumentException('out-of-range bits detected');
744
        }
745
746 2
        $this->bitset = $int;
747 2
    }
748
749
    /**
750
     * Get binary bitset in big-endian order
751
     * 
752
     * @return string
753
     */
754 1
    public function getBinaryBitsetBe(): string
755
    {
756 1
        return \strrev($this->bitset);
757
    }
758
759
    /**
760
     * Set binary bitset in big-endian order
761
     *
762
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
763
     * @return static
0 ignored issues
show
Documentation Bug introduced by
The doc comment static at position 0 could not be parsed: 'static' is only available from within classes.
Loading history...
764
     * @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...
765
     */
766 1
    public function withBinaryBitsetBe(string $bitset): self
767
    {
768 1
        return $this->withBinaryBitsetLe(\strrev($bitset));
769
    }
770
771
    /**
772
     * Get a bit at the given ordinal number
773
     *
774
     * @param int $ordinal Ordinal number of bit to get
2 ignored issues
show
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
775
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
776
     * @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...
777
     * @uses doGetBitBin()
778
     * @uses doGetBitInt()
779
     */
780 35
    public function getBit(int $ordinal): bool
781
    {
782 35
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
783 1
            throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}");
1 ignored issue
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...
Coding Style introduced by
This line exceeds maximum limit of 100 characters; contains 113 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
784
        }
785
786 34
        return $this->{$this->fnDoGetBit}($ordinal);
787
    }
788
789
    /**
790
     * Get a bit at the given ordinal number.
791
     *
792
     * This is the binary bitset implementation.
793
     *
794
     * @param int $ordinal Ordinal number of bit to get
2 ignored issues
show
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
795
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
796
     * @see getBit()
797
     * @see doGetBitInt()
798
     */
799 14
    private function doGetBitBin(int $ordinal): bool
800
    {
801 14
        return (\ord($this->bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
802
    }
803
804
    /**
805
     * Get a bit at the given ordinal number.
806
     *
807
     * This is the integer bitset implementation.
808
     * 
809
     * @param int $ordinal Ordinal number of bit to get
2 ignored issues
show
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
810
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
811
     * @see getBit()
812
     * @see doGetBitBin()
813
     */
814 25
    private function doGetBitInt(int $ordinal): bool
815
    {
816 25
        return (bool)($this->bitset & (1 << $ordinal));
817
    }
818
819
    /**
820
     * Set a bit at the given ordinal number
821
     *
822
     * @param int $ordinal Ordinal number of bit to set
2 ignored issues
show
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
823
     * @param bool $bit    The bit to set
2 ignored issues
show
Coding Style introduced by
Expected "boolean" but found "bool" for parameter type
Loading history...
Coding Style introduced by
Expected 5 spaces after parameter name; 4 found
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
824
     * @return static
0 ignored issues
show
Documentation Bug introduced by
The doc comment static at position 0 could not be parsed: 'static' is only available from within classes.
Loading history...
825
     * @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...
826
     * @uses doSetBitBin()
827
     * @uses doSetBitInt()
828
     * @uses doUnsetBitBin()
829
     * @uses doUnsetBitInt()
830
     */
831 2
    public function withBit(int $ordinal, bool $bit): self
832
    {
833 2
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
834 1
            throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}");
1 ignored issue
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...
Coding Style introduced by
This line exceeds maximum limit of 100 characters; contains 113 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
835
        }
836
837 1
        $clone = clone $this;
838 1
        if ($bit) {
839 1
            $clone->{$this->fnDoSetBit}($ordinal);
840
        } else {
841 1
            $clone->{$this->fnDoUnsetBit}($ordinal);
842
        }
843 1
        return $clone;
844
    }
845
846
    /**
847
     * Set a bit at the given ordinal number.
848
     *
849
     * This is the binary bitset implementation.
850
     * 
851
     * @param int $ordinal Ordinal number of bit to set
2 ignored issues
show
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
852
     * @return void
853
     * @see setBit()
854
     * @see doSetBitInt()
855
     */
856 12
    private function doSetBitBin(int $ordinal): void
857
    {
858 12
        $byte = (int) ($ordinal / 8);
859 12
        $this->bitset[$byte] = $this->bitset[$byte] | \chr(1 << ($ordinal % 8));
860 12
    }
861
862
    /**
863
     * Set a bit at the given ordinal number.
864
     *
865
     * This is the binary bitset implementation.
866
     *
867
     * @param int $ordinal Ordinal number of bit to set
2 ignored issues
show
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
868
     * @return void
869
     * @see setBit()
870
     * @see doSetBitBin()
871
     */
872 39
    private function doSetBitInt(int $ordinal): void
873
    {
874 39
        $this->bitset = $this->bitset | (1 << $ordinal);
875 39
    }
876
877
    /**
878
     * Unset a bit at the given ordinal number.
879
     *
880
     * This is the binary bitset implementation.
881
     *
882
     * @param int $ordinal Ordinal number of bit to unset
2 ignored issues
show
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
883
     * @return void
884
     * @see setBit()
885
     * @see doUnsetBitInt()
886
     */
887 2
    private function doUnsetBitBin(int $ordinal): void
888
    {
889 2
        $byte = (int) ($ordinal / 8);
890 2
        $this->bitset[$byte] = $this->bitset[$byte] & \chr(~(1 << ($ordinal % 8)));
891 2
    }
892
893
    /**
894
     * Unset a bit at the given ordinal number.
895
     *
896
     * This is the integer bitset implementation.
897
     *
898
     * @param int $ordinal Ordinal number of bit to unset
2 ignored issues
show
Coding Style introduced by
Expected "integer" but found "int" for parameter type
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
899
     * @return void
900
     * @see setBit()
901
     * @see doUnsetBitBin()
902
     */
903 3
    private function doUnsetBitInt(int $ordinal): void
904
    {
905 3
        $this->bitset = $this->bitset & ~(1 << $ordinal);
906 3
    }
907
}
908