Test Failed
Push — EnumSet-iterator-aggregate ( 8cdcbc )
by Marc
04:13
created

EnumSet.php$0 ➔ __construct()   A

Complexity

Conditions 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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