Passed
Push — immutable-set ( 4c41ff...8b1b82 )
by Marc
03:40
created

EnumSet::withEnumerators()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
ccs 0
cts 5
cp 0
crap 6
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 $fnDoGetIterator       = 'doGetIteratorInt';
1 ignored issue
show
Coding Style introduced by
Private member variable "fnDoGetIterator" must contain a leading underscore
Loading history...
49
    private $fnDoCount             = 'doCountInt';
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 "fnDoCount" must contain a leading underscore
Loading history...
50
    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...
51
    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...
52
    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...
53
    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...
54
    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...
55
    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...
56
    /**#@-*/
57
58
    /**
59
     * Constructor
60
     *
61
     * @param string $enumeration The classname of the enumeration
1 ignored issue
show
introduced by
Parameter comment must end with a full stop
Loading history...
Coding Style introduced by
Expected 8 spaces after parameter type; 1 found
Loading history...
62
     * @param iterable|null $enumerators iterable list of enumerators initializing the set
1 ignored issue
show
introduced by
Parameter comment must start with a capital letter
Loading history...
introduced by
Parameter comment must end with a full stop
Loading history...
63
     * @throws InvalidArgumentException
0 ignored issues
show
introduced by
Comment missing for @throws tag in function comment
Loading history...
64
     */
65 65
    public function __construct(string $enumeration, iterable $enumerators = null)
2 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$enumerators" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$enumerators"; expected 0 but found 1
Loading history...
66
    {
67 65
        if (!\is_subclass_of($enumeration, Enum::class)) {
68 1
            throw new InvalidArgumentException(\sprintf(
69 1
                '%s can handle subclasses of %s only',
70 1
                __METHOD__,
71 1
                Enum::class
72
            ));
73
        }
74
75 64
        $this->enumeration      = $enumeration;
76 64
        $this->enumerationCount = \count($enumeration::getConstants());
77
78
        // By default the bitset is initialized as integer bitset
79
        // in case the enumeraton has more enumerators then integer bits
80
        // we will switch this into a binary bitset
81 64
        if ($this->enumerationCount > \PHP_INT_SIZE * 8) {
82
            // init binary bitset with zeros
83 15
            $this->bitset = \str_repeat("\0", (int)\ceil($this->enumerationCount / 8));
84
85
            // switch internal binary bitset functions
86 15
            $this->fnDoGetIterator       = 'doGetIteratorBin';
87 15
            $this->fnDoCount             = 'doCountBin';
88 15
            $this->fnDoGetOrdinals       = 'doGetOrdinalsBin';
89 15
            $this->fnDoGetBit            = 'doGetBitBin';
90 15
            $this->fnDoSetBit            = 'doSetBitBin';
91 15
            $this->fnDoUnsetBit          = 'doUnsetBitBin';
92 15
            $this->fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeBin';
93 15
            $this->fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeBin';
94
        }
95
96 64
        if ($enumerators !== null) {
97
            foreach ($enumerators as $enumerator) {
98
                $this->{$this->fnDoSetBit}($enumeration::get($enumerator)->getOrdinal());
99
            }
100
        }
101 64
    }
102
103
    /**
104
     * Get the classname of the enumeration
105
     * @return string
106
     */
107 1
    public function getEnumeration(): string
108
    {
109 1
        return $this->enumeration;
110
    }
111
112
    /**
113
     * Adds the given enumerator
114
     * @param Enum|null|bool|int|float|string|array $enumerator Enumerator object or value to remove
2 ignored issues
show
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...
introduced by
Parameter comment must end with a full stop
Loading history...
115
     * @return static
116
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
117
     */
118 41
    public function withEnumerator($enumerator): self
119
    {
120 41
        $clone = clone $this;
121 41
        $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
122 41
        return $clone;
123
    }
124
125
    /**
126
     * Adds the given enumerators
127
     * @param iterable $enumerators Iterable list of enumerator objects or values to add
1 ignored issue
show
introduced by
Parameter comment must end with a full stop
Loading history...
128
     * @return static
129
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
130
     */
131
    public function withEnumerators(iterable $enumerators): self
132
    {
133
        $clone = clone $this;
134
        foreach ($enumerators as $enumerator) {
135
            $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
136
        }
137
        return $clone;
138
    }
139
140
    /**
141
     * Removes an enumerator
142
     * @param Enum|null|bool|int|float|string|array $enumerator Enumerator object or value to remove
2 ignored issues
show
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...
introduced by
Parameter comment must end with a full stop
Loading history...
143
     * @return static
144
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
145
     */
146 4
    public function withoutEnumerator($enumerator): self
147
    {
148 4
        $clone = clone $this;
149 4
        $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
150 4
        return $clone;
151
    }
152
153
    /**
154
     * Removes enumerators
155
     * @param iterable $enumerators Iterable list of enumerator objects or values to remove
1 ignored issue
show
introduced by
Parameter comment must end with a full stop
Loading history...
156
     * @return static
157
     * @throws InvalidArgumentException On an invalid given enumerator
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
158
     */
159
    public function withoutEnumerators(iterable $enumerators): self
160
    {
161
        $clone = clone $this;
162
        foreach ($enumerators as $enumerator) {
163
            $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
164
        }
165
        return $clone;
166
    }
167
168
    /**
169
     * Test if the given enumerator exists
170
     * @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...
171
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
172
     */
173 11
    public function contains($enumerator): bool
174
    {
175 11
        return $this->{$this->fnDoGetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
176
    }
177
178
    /* IteratorAggregate */
179
180
    /**
181
     * Get a new iterator
182
     * @return Iterator
183
     * @uses doGetIteratorInt()
184
     * @uses doGetIteratorBin()
185
     */
186 17
    public function getIterator(): Iterator
187
    {
188 17
        return $this->{$this->fnDoGetIterator}();
189
    }
190
191
    /**
192
     * Get a new Iterator.
193
     *
194
     * This is the binary bitset implementation.
195
     *
196
     * @return Iterator
197
     * @see getIterator()
198
     * @see goGetIteratorInt()
199
     */
200 5
    private function doGetIteratorBin()
201
    {
202 5
        $bitset   = $this->bitset;
203 5
        $byteLen  = \strlen($bitset);
204 5
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
205 5
            if ($bitset[$bytePos] === "\0") {
206
                // fast skip null byte
207 3
                continue;
208
            }
209
210 5
            $ord = \ord($bitset[$bytePos]);
211 5
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
212 5
                if ($ord & (1 << $bitPos)) {
213 5
                    $ordinal = $bytePos * 8 + $bitPos;
214 5
                    yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
215
                }
216
            }
217
        }
218 5
    }
219
220
    /**
221
     * Get a new Iterator.
222
     *
223
     * This is the integer bitset implementation.
224
     *
225
     * @return Iterator
226
     * @see getIterator()
227
     * @see doGetIteratorBin()
228
     */
229 12
    private function doGetIteratorInt()
230
    {
231 12
        $count  = $this->enumerationCount;
232 12
        $bitset = $this->bitset;
233 12
        for ($ordinal = 0; $ordinal < $count; ++$ordinal) {
234 12
            if ($bitset & (1 << $ordinal)) {
235 10
                yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
236
            }
237
        }
238 10
    }
239
240
    /* Countable */
241
242
    /**
243
     * Count the number of elements
244
     *
245
     * @return int
1 ignored issue
show
Coding Style introduced by
Expected "integer" but found "int" for function return type
Loading history...
246
     * @uses doCountBin()
247
     * @uses doCountInt()
248
     */
249 15
    public function count(): int
250
    {
251 15
        return $this->{$this->fnDoCount}();
252
    }
253
254
    /**
255
     * Count the number of elements.
256
     *
257
     * This is the binary bitset implementation.
258
     *
259
     * @return int
1 ignored issue
show
Coding Style introduced by
Expected "integer" but found "int" for function return type
Loading history...
260
     * @see count()
261
     * @see doCountInt()
262
     */
263 7
    private function doCountBin()
264
    {
265 7
        $count   = 0;
266 7
        $bitset  = $this->bitset;
267 7
        $byteLen = \strlen($bitset);
268 7
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
269 7
            if ($bitset[$bytePos] === "\0") {
270
                // fast skip null byte
271 5
                continue;
272
            }
273
274 7
            $ord = \ord($bitset[$bytePos]);
275 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...
276 7
            if ($ord & 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...
277 7
            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...
278 7
            if ($ord & 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...
279 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...
280 7
            if ($ord & 0b00100000) ++$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...
281 7
            if ($ord & 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...
282 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...
283
        }
284 7
        return $count;
285
    }
286
287
    /**
288
     * Count the number of elements.
289
     *
290
     * This is the integer bitset implementation.
291
     *
292
     * @return int
1 ignored issue
show
Coding Style introduced by
Expected "integer" but found "int" for function return type
Loading history...
293
     * @see count()
294
     * @see doCountBin()
295
     */
296 8
    private function doCountInt()
297
    {
298 8
        $count  = 0;
299 8
        $bitset = $this->bitset;
300
301
        // PHP does not support right shift unsigned
302 8
        if ($bitset < 0) {
303 2
            $count  = 1;
304 2
            $bitset = $bitset & \PHP_INT_MAX;
305
        }
306
307
        // iterate byte by byte and count set bits
308 8
        $phpIntBitSize = \PHP_INT_SIZE * 8;
309 8
        for ($bitPos = 0; $bitPos < $phpIntBitSize; $bitPos += 8) {
310 8
            $bitChk = 0xff << $bitPos;
311 8
            $byte = $bitset & $bitChk;
312 8
            if ($byte) {
313 7
                $byte = $byte >> $bitPos;
314 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...
315 7
                if ($byte & 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...
316 7
                if ($byte & 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...
317 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...
318 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...
319 7
                if ($byte & 0b00100000) ++$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...
320 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...
321 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...
322
            }
323
324 8
            if ($bitset <= $bitChk) {
325 7
                break;
326
            }
327
        }
328
329 8
        return $count;
330
    }
331
332
    /**
333
     * Check if this EnumSet is the same as other
334
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
335
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
336
     */
337 3
    public function isEqual(EnumSet $other): bool
338
    {
339 3
        return $this->enumeration === $other->enumeration
340 3
            && $this->bitset === $other->bitset;
341
    }
342
343
    /**
344
     * Check if this EnumSet is a subset of other
345
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
346
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
347
     */
348 4
    public function isSubset(EnumSet $other): bool
349
    {
350 4
        return $this->enumeration === $other->enumeration
351 4
            && ($this->bitset & $other->bitset) === $this->bitset;
352
    }
353
354
    /**
355
     * Check if this EnumSet is a superset of other
356
     * @param EnumSet $other
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
357
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
358
     */
359 4
    public function isSuperset(EnumSet $other): bool
360
    {
361 4
        return $this->enumeration === $other->enumeration
362 4
            && ($this->bitset | $other->bitset) === $this->bitset;
363
    }
364
365
    /**
366
     * Produce a new set with enumerators from both this and other (this | other)
367
     *
368
     * @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...
369
     * @return static
370
     * @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...
371
     */
372 2
    public function union(EnumSet $other): self
373
    {
374 2
        if ($this->enumeration !== $other->enumeration) {
375 1
            throw new InvalidArgumentException(\sprintf(
376 1
                'Other should be of the same enumeration as this %s',
377 1
                $this->enumeration
378
            ));
379
        }
380
381 1
        $clone = clone $this;
382 1
        $clone->bitset = $this->bitset | $other->bitset;
383 1
        return $clone;
384
    }
385
386
    /**
387
     * Produce a new set with enumerators common to both this and other (this & other)
388
     *
389
     * @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...
390
     * @return static
391
     * @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...
392
     */
393 2
    public function intersect(EnumSet $other): self
394
    {
395 2
        if ($this->enumeration !== $other->enumeration) {
396 1
            throw new InvalidArgumentException(\sprintf(
397 1
                'Other should be of the same enumeration as this %s',
398 1
                $this->enumeration
399
            ));
400
        }
401
402 1
        $clone = clone $this;
403 1
        $clone->bitset = $this->bitset & $other->bitset;
404 1
        return $clone;
405
    }
406
407
    /**
408
     * Produce a new set with enumerators in this but not in other (this - other)
409
     *
410
     * @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...
411
     * @return static
412
     * @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...
413
     */
414 2
    public function diff(EnumSet $other): self
415
    {
416 2
        if ($this->enumeration !== $other->enumeration) {
417 1
            throw new InvalidArgumentException(\sprintf(
418 1
                'Other should be of the same enumeration as this %s',
419 1
                $this->enumeration
420
            ));
421
        }
422
423 1
        $clone = clone $this;
424 1
        $clone->bitset = $this->bitset & ~$other->bitset;
425 1
        return $clone;
426
    }
427
428
    /**
429
     * Produce a new set with enumerators in either this and other but not in both (this ^ other)
430
     *
431
     * @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...
432
     * @return static
433
     * @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...
434
     */
435 2
    public function symDiff(EnumSet $other): self
436
    {
437 2
        if ($this->enumeration !== $other->enumeration) {
438 1
            throw new InvalidArgumentException(\sprintf(
439 1
                'Other should be of the same enumeration as this %s',
440 1
                $this->enumeration
441
            ));
442
        }
443
444 1
        $clone = clone $this;
445 1
        $clone->bitset = $this->bitset ^ $other->bitset;
446 1
        return $clone;
447
    }
448
449
    /**
450
     * Get ordinal numbers of the defined enumerators as array
451
     * @return int[]
452
     * @uses  doGetOrdinalsBin()
453
     * @uses  doGetOrdinalsInt()
454
     */
455 9
    public function getOrdinals(): array
456
    {
457 9
        return $this->{$this->fnDoGetOrdinals}();
458
    }
459
460
    /**
461
     * Get ordinal numbers of the defined enumerators as array.
462
     *
463
     * This is the binary bitset implementation.
464
     *
465
     * @return int[]
466
     * @see getOrdinals()
467
     * @see goGetOrdinalsInt()
468
     */
469 1
    private function doGetOrdinalsBin()
470
    {
471 1
        $ordinals = [];
472 1
        $bitset   = $this->bitset;
473 1
        $byteLen  = \strlen($bitset);
474 1
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
475 1
            if ($bitset[$bytePos] === "\0") {
476
                // fast skip null byte
477 1
                continue;
478
            }
479
480 1
            $ord = \ord($bitset[$bytePos]);
481 1
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
482 1
                if ($ord & (1 << $bitPos)) {
483 1
                    $ordinals[] = $bytePos * 8 + $bitPos;
484
                }
485
            }
486
        }
487 1
        return $ordinals;
488
    }
489
490
    /**
491
     * Get ordinal numbers of the defined enumerators as array.
492
     *
493
     * This is the integer bitset implementation.
494
     *
495
     * @return int[]
496
     * @see getOrdinals()
497
     * @see doGetOrdinalsBin()
498
     */
499 8
    private function doGetOrdinalsInt()
500
    {
501 8
        $ordinals = [];
502 8
        $count    = $this->enumerationCount;
503 8
        $bitset   = $this->bitset;
504 8
        for ($ordinal = 0; $ordinal < $count; ++$ordinal) {
505 8
            if ($bitset & (1 << $ordinal)) {
506 8
                $ordinals[] = $ordinal;
507
            }
508
        }
509 8
        return $ordinals;
510
    }
511
512
    /**
513
     * Get values of the defined enumerators as array
514
     * @return mixed[]
515
     */
516 5
    public function getValues(): array
517
    {
518 5
        $enumeration = $this->enumeration;
519 5
        $values      = [];
520 5
        foreach ($this->getOrdinals() as $ord) {
521 5
            $values[] = $enumeration::byOrdinal($ord)->getValue();
522
        }
523 5
        return $values;
524
    }
525
526
    /**
527
     * Get names of the defined enumerators as array
528
     * @return string[]
529
     */
530 1
    public function getNames(): array
531
    {
532 1
        $enumeration = $this->enumeration;
533 1
        $names       = [];
534 1
        foreach ($this->getOrdinals() as $ord) {
535 1
            $names[] = $enumeration::byOrdinal($ord)->getName();
536
        }
537 1
        return $names;
538
    }
539
540
    /**
541
     * Get the defined enumerators as array
542
     * @return Enum[]
543
     */
544 1
    public function getEnumerators(): array
545
    {
546 1
        $enumeration = $this->enumeration;
547 1
        $enumerators = [];
548 1
        foreach ($this->getOrdinals() as $ord) {
549 1
            $enumerators[] = $enumeration::byOrdinal($ord);
550
        }
551 1
        return $enumerators;
552
    }
553
554
    /**
555
     * Get binary bitset in little-endian order
556
     * 
557
     * @return string
558
     * @uses doGetBinaryBitsetLeBin()
559
     * @uses doGetBinaryBitsetLeInt()
560
     */
561 6
    public function getBinaryBitsetLe(): string
562
    {
563 6
        return $this->{$this->fnDoGetBinaryBitsetLe}();
564
    }
565
566
    /**
567
     * Get binary bitset in little-endian order.
568
     *
569
     * This is the binary bitset implementation.
570
     *
571
     * @return string
572
     * @see getBinaryBitsetLe()
573
     * @see doGetBinaryBitsetLeInt()
574
     */
575 4
    private function doGetBinaryBitsetLeBin()
576
    {
577 4
        return $this->bitset;
578
    }
579
580
    /**
581
     * Get binary bitset in little-endian order.
582
     *
583
     * This is the integer bitset implementation.
584
     *
585
     * @return string
586
     * @see getBinaryBitsetLe()
587
     * @see doGetBinaryBitsetLeBin()
588
     */
589 2
    private function doGetBinaryBitsetLeInt()
590
    {
591 2
        $bin = \pack(\PHP_INT_SIZE === 8 ? 'P' : 'V', $this->bitset);
592 2
        return \substr($bin, 0, (int)\ceil($this->enumerationCount / 8));
593
    }
594
595
    /**
596
     * Set binary bitset in little-endian order
597
     *
598
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
599
     * @return static
600
     * @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...
601
     * @uses doSetBinaryBitsetLeBin()
602
     * @uses doSetBinaryBitsetLeInt()
603
     */
604 12
    public function withBinaryBitsetLe(string $bitset): self
605
    {
606 12
        $clone = clone $this;
607 12
        $clone->{$this->fnDoSetBinaryBitsetLe}($bitset);
608 6
        return $clone;
609
    }
610
611
    /**
612
     * Set binary bitset in little-endian order
613
     *
614
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
615
     * @return void
616
     * @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...
617
     * @see setBinaryBitsetLeBin()
618
     * @see doSetBinaryBitsetLeInt()
619
     */
620 7
    private function doSetBinaryBitsetLeBin($bitset): void
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $bitset
Loading history...
621
    {
622 7
        $size   = \strlen($this->bitset);
623 7
        $sizeIn = \strlen($bitset);
624
625 7
        if ($sizeIn < $size) {
626
            // add "\0" if the given bitset is not long enough
627 1
            $bitset .= \str_repeat("\0", $size - $sizeIn);
628 6
        } elseif ($sizeIn > $size) {
629 2
            if (\ltrim(\substr($bitset, $size), "\0") !== '') {
630 1
                throw new InvalidArgumentException('out-of-range bits detected');
631
            }
632 1
            $bitset = \substr($bitset, 0, $size);
633
        }
634
635
        // truncate out-of-range bits of last byte
636 6
        $lastByteMaxOrd = $this->enumerationCount % 8;
637 6
        if ($lastByteMaxOrd !== 0) {
638 6
            $lastByte         = $bitset[-1];
639 6
            $lastByteExpected = \chr((1 << $lastByteMaxOrd) - 1) & $lastByte;
640 6
            if ($lastByte !== $lastByteExpected) {
641 2
                throw new InvalidArgumentException('out-of-range bits detected');
642
            }
643
644 4
            $this->bitset = \substr($bitset, 0, -1) . $lastByteExpected;
645
        }
646
647 4
        $this->bitset = $bitset;
648 4
    }
649
650
    /**
651
     * Set binary bitset in little-endian order
652
     *
653
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
654
     * @return void
655
     * @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...
656
     * @see setBinaryBitsetLeBin()
657
     * @see doSetBinaryBitsetLeBin()
658
     */
659 5
    private function doSetBinaryBitsetLeInt($bitset): void
0 ignored issues
show
Coding Style introduced by
Type hint "string" missing for $bitset
Loading history...
660
    {
661 5
        $len = \strlen($bitset);
662 5
        $int = 0;
663 5
        for ($i = 0; $i < $len; ++$i) {
664 5
            $ord = \ord($bitset[$i]);
665
666 5
            if ($ord && $i > \PHP_INT_SIZE - 1) {
667 1
                throw new InvalidArgumentException('out-of-range bits detected');
668
            }
669
670 5
            $int |= $ord << (8 * $i);
671
        }
672
673 4
        if ($int & (~0 << $this->enumerationCount)) {
674 2
            throw new InvalidArgumentException('out-of-range bits detected');
675
        }
676
677 2
        $this->bitset = $int;
678 2
    }
679
680
    /**
681
     * Get binary bitset in big-endian order
682
     * 
683
     * @return string
684
     */
685 1
    public function getBinaryBitsetBe(): string
686
    {
687 1
        return \strrev($this->bitset);
688
    }
689
690
    /**
691
     * Set binary bitset in big-endian order
692
     *
693
     * @param string $bitset
0 ignored issues
show
Documentation introduced by
Missing parameter comment
Loading history...
694
     * @return static
695
     * @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...
696
     */
697 1
    public function withBinaryBitsetBe(string $bitset): self
698
    {
699 1
        return $this->withBinaryBitsetLe(\strrev($bitset));
700
    }
701
702
    /**
703
     * Get a bit at the given ordinal number
704
     *
705
     * @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...
706
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
707
     * @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...
708
     * @uses doGetBitBin()
709
     * @uses doGetBitInt()
710
     */
711 3
    public function getBit(int $ordinal): bool
712
    {
713 3
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
714 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...
715
        }
716
717 2
        return $this->{$this->fnDoGetBit}($ordinal);
718
    }
719
720
    /**
721
     * Get a bit at the given ordinal number.
722
     *
723
     * This is the binary bitset implementation.
724
     *
725
     * @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...
726
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
727
     * @see getBit()
728
     * @see doGetBitInt()
729
     */
730 5
    private function doGetBitBin($ordinal)
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
731
    {
732 5
        return (\ord($this->bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
733
    }
734
735
    /**
736
     * Get a bit at the given ordinal number.
737
     *
738
     * This is the integer bitset implementation.
739
     * 
740
     * @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...
741
     * @return bool
1 ignored issue
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
742
     * @see getBit()
743
     * @see doGetBitBin()
744
     */
745 7
    private function doGetBitInt($ordinal)
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
746
    {
747 7
        return (bool)($this->bitset & (1 << $ordinal));
748
    }
749
750
    /**
751
     * Set a bit at the given ordinal number
752
     *
753
     * @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...
754
     * @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...
755
     * @return static
756
     * @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...
757
     * @uses doSetBitBin()
758
     * @uses doSetBitInt()
759
     * @uses doUnsetBitBin()
760
     * @uses doUnsetBitInt()
761
     */
762 2
    public function withBit(int $ordinal, bool $bit): self
763
    {
764 2
        if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
765 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...
766
        }
767
768 1
        $clone = clone $this;
769 1
        if ($bit) {
770 1
            $clone->{$this->fnDoSetBit}($ordinal);
771
        } else {
772 1
            $clone->{$this->fnDoUnsetBit}($ordinal);
773
        }
774 1
        return $clone;
775
    }
776
777
    /**
778
     * Set a bit at the given ordinal number.
779
     *
780
     * This is the binary bitset implementation.
781
     * 
782
     * @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...
783
     * @return void
784
     * @see setBit()
785
     * @see doSetBitInt()
786
     */
787 8
    private function doSetBitBin($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
788
    {
789 8
        $byte = (int) ($ordinal / 8);
790 8
        $this->bitset[$byte] = $this->bitset[$byte] | \chr(1 << ($ordinal % 8));
791 8
    }
792
793
    /**
794
     * Set a bit at the given ordinal number.
795
     *
796
     * This is the binary bitset implementation.
797
     *
798
     * @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...
799
     * @return void
800
     * @see setBit()
801
     * @see doSetBitBin()
802
     */
803 34
    private function doSetBitInt($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
804
    {
805 34
        $this->bitset = $this->bitset | (1 << $ordinal);
806 34
    }
807
808
    /**
809
     * Unset a bit at the given ordinal number.
810
     *
811
     * This is the binary bitset implementation.
812
     *
813
     * @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...
814
     * @return void
815
     * @see setBit()
816
     * @see doUnsetBitInt()
817
     */
818 2
    private function doUnsetBitBin($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
819
    {
820 2
        $byte = (int) ($ordinal / 8);
821 2
        $this->bitset[$byte] = $this->bitset[$byte] & \chr(~(1 << ($ordinal % 8)));
822 2
    }
823
824
    /**
825
     * Unset a bit at the given ordinal number.
826
     *
827
     * This is the integer bitset implementation.
828
     *
829
     * @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...
830
     * @return void
831
     * @see setBit()
832
     * @see doUnsetBitBin()
833
     */
834 3
    private function doUnsetBitInt($ordinal): void
0 ignored issues
show
Coding Style introduced by
Type hint "int" missing for $ordinal
Loading history...
835
    {
836 3
        $this->bitset = $this->bitset & ~(1 << $ordinal);
837 3
    }
838
}
839