Completed
Push — feature/bench ( a60088...b33c92 )
by Marc
27:03
created

EnumSet::unsetBit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 1 Features 0
Metric Value
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
c 1
b 1
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
3
namespace MabeEnum;
4
5
use Iterator;
6
use Countable;
7
use InvalidArgumentException;
8
9
/**
10
 * This EnumSet is based on a bitset of a binary string.
11
 *
12
 * @link http://github.com/marc-mabe/php-enum for the canonical source repository
13
 * @copyright Copyright (c) 2015 Marc Bennewitz
14
 * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
15
 */
16
class EnumSet implements Iterator, Countable
17
{
18
    /**
19
     * The classname of the Enumeration
20
     * @var string
21
     */
22
    private $enumeration;
23
24
    /**
25
     * BitSet of all attached enumerations in little endian
26
     * @var string
27
     */
28
    private $bitset;
29
30
    /**
31
     * Ordinal number of current iterator position
32
     * @var int
33
     */
34
    private $ordinal = 0;
35
36
    /**
37
     * Highest possible ordinal number
38
     * @var int
39
     */
40
    private $ordinalMax;
41
42
    /**
43
     * Constructor
44
     *
45
     * @param string $enumeration The classname of the enumeration
46
     * @throws InvalidArgumentException
47
     */
48 50
    public function __construct($enumeration)
49
    {
50 50 View Code Duplication
        if (!is_subclass_of($enumeration, __NAMESPACE__ . '\Enum')) {
51 1
            throw new InvalidArgumentException(sprintf(
52 1
                "This EnumSet can handle subclasses of '%s' only",
53
                __NAMESPACE__ . '\Enum'
54 1
            ));
55
        }
56
        
57 49
        $this->enumeration = $enumeration;
58 49
        $this->ordinalMax  = count($enumeration::getConstants());
59
        
60
        // init the bitset with zeros
61 49
        $this->bitset = str_repeat("\0", ceil($this->ordinalMax / 8));
62 49
    }
63
64
    /**
65
     * Get the classname of the enumeration
66
     * @return string
67
     */
68 1
    public function getEnumeration()
69
    {
70 1
        return $this->enumeration;
71
    }
72
73
    /**
74
     * Attach a new enumerator or overwrite an existing one
75
     * @param Enum|null|boolean|int|float|string $enumerator
76
     * @return void
77
     * @throws InvalidArgumentException On an invalid given enumerator
78
     */
79 34
    public function attach($enumerator)
80
    {
81 34
        $enumeration = $this->enumeration;
82 34
        $this->setBit($enumeration::get($enumerator)->getOrdinal());
83 34
    }
84
85
    /**
86
     * Detach the given enumerator
87
     * @param Enum|null|boolean|int|float|string $enumerator
88
     * @return void
89
     * @throws InvalidArgumentException On an invalid given enumerator
90
     */
91 6
    public function detach($enumerator)
92
    {
93 6
        $enumeration = $this->enumeration;
94 6
        $this->unsetBit($enumeration::get($enumerator)->getOrdinal());
95 6
    }
96
97
    /**
98
     * Test if the given enumerator was attached
99
     * @param Enum|null|boolean|int|float|string $enumerator
100
     * @return boolean
101
     */
102 9
    public function contains($enumerator)
103
    {
104 9
        $enumeration = $this->enumeration;
105 9
        return $this->getBit($enumeration::get($enumerator)->getOrdinal());
106
    }
107
108
    /* Iterator */
109
110
    /**
111
     * Get the current enumerator
112
     * @return Enum|null Returns the current enumerator or NULL on an invalid iterator position
113
     */
114 9
    public function current()
115
    {
116 9
        if ($this->valid()) {
117 9
            $enumeration = $this->enumeration;
118 9
            return $enumeration::byOrdinal($this->ordinal);
119
        }
120
121 2
        return null;
122
    }
123
124
    /**
125
     * Get the ordinal number of the current iterator position
126
     * @return int
127
     */
128 6
    public function key()
129
    {
130 6
        return $this->ordinal;
131
    }
132
133
    /**
134
     * Go to the next valid iterator position.
135
     * If no valid iterator position is found the iterator position will be the last possible + 1.
136
     * @return void
137
     */
138 15
    public function next()
139
    {
140
        do {
141 15
            if (++$this->ordinal >= $this->ordinalMax) {
142 4
                $this->ordinal = $this->ordinalMax;
143 4
                return;
144
            }
145 15
        } while (!$this->getBit($this->ordinal));
146 15
    }
147
148
    /**
149
     * Go to the first valid iterator position.
150
     * If no valid iterator position in found the iterator position will be 0.
151
     * @return void
152
     */
153 9
    public function rewind()
154
    {
155 9
        if (trim($this->bitset, "\0") !== '') {
156 9
            $this->ordinal = -1;
157 9
            $this->next();
158 9
        } else {
159 1
            $this->ordinal = 0;
160
        }
161 9
    }
162
163
    /**
164
     * Test if the iterator in a valid state
165
     * @return boolean
166
     */
167 10
    public function valid()
168
    {
169 10
        return $this->ordinal !== $this->ordinalMax && $this->getBit($this->ordinal);
170
    }
171
172
    /* Countable */
173
174
    /**
175
     * Count the number of elements
176
     * @return int
177
     */
178 9
    public function count()
179
    {
180 9
        $count   = 0;
181 9
        $byteLen = strlen($this->bitset);
182 9
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
183 8
            if ($this->bitset[$bytePos] === "\0") {
184 6
                continue; // fast skip null byte
185
            }
186
187 8
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
188 8
                if ((ord($this->bitset[$bytePos]) & (1 << $bitPos)) !== 0) {
189 8
                    ++$count;
190 8
                }
191 8
            }
192 8
        }
193
194 9
        return $count;
195
    }
196
197
    /**
198
     * Check if this EnumSet is the same as other
199
     * @param EnumSet $other
200
     * @return bool
201
     */
202 3
    public function isEqual(EnumSet $other)
203
    {
204 3
        return $this->enumeration === $other->enumeration
205 3
            && $this->bitset === $other->bitset;
206
    }
207
208
    /**
209
     * Check if this EnumSet is a subset of other
210
     * @param EnumSet $other
211
     * @return bool
212
     */
213 4 View Code Duplication
    public function isSubset(EnumSet $other)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
214
    {
215 4
        if ($this->enumeration !== $other->enumeration) {
216 1
            return false;
217
        }
218
219 3
        return ($this->bitset & $other->bitset) === $this->bitset;
220
    }
221
222
    /**
223
     * Check if this EnumSet is a superset of other
224
     * @param EnumSet $other
225
     * @return bool
226
     */
227 4 View Code Duplication
    public function isSuperset(EnumSet $other)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
228
    {
229 4
        if ($this->enumeration !== $other->enumeration) {
230 1
            return false;
231
        }
232
233 3
        return ($this->bitset | $other->bitset) === $this->bitset;
234
    }
235
236
    /**
237
     * Produce a new set with enumerators from both this and other (this | other)
238
     * @param EnumSet ...$other Other EnumSet(s) of the same enumeration to produce the union
239
     * @return EnumSet
240
     */
241 2 View Code Duplication
    public function union(EnumSet $other)
0 ignored issues
show
Unused Code introduced by
The parameter $other is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
242
    {
243 2
        $bitset = $this->bitset;
244 2
        foreach (func_get_args() as $other) {
245 2
            if (!$other instanceof self || $this->enumeration !== $other->enumeration) {
246 1
                throw new InvalidArgumentException(sprintf(
247 1
                    "Others should be an instance of %s of the same enumeration as this %s",
248 1
                    __CLASS__,
249 1
                    $this->enumeration
250 1
                ));
251
            }
252
253 1
            $bitset |= $other->bitset;
254 1
        }
255
256 1
        $clone = clone $this;
257 1
        $clone->bitset = $bitset;
258 1
        return $clone;
259
    }
260
261
    /**
262
     * Produce a new set with enumerators common to both this and other (this & other)
263
     * @param EnumSet ...$other Other EnumSet(s) of the same enumeration to produce the union
264
     * @return EnumSet
265
     */
266 2 View Code Duplication
    public function intersect(EnumSet $other)
0 ignored issues
show
Unused Code introduced by
The parameter $other is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
267
    {
268 2
        $bitset = $this->bitset;
269 2
        foreach (func_get_args() as $other) {
270 2
            if (!$other instanceof self || $this->enumeration !== $other->enumeration) {
271 1
                throw new InvalidArgumentException(sprintf(
272 1
                    "Others should be an instance of %s of the same enumeration as this %s",
273 1
                    __CLASS__,
274 1
                    $this->enumeration
275 1
                ));
276
            }
277
278 1
            $bitset &= $other->bitset;
279 1
        }
280
281 1
        $clone = clone $this;
282 1
        $clone->bitset = $bitset;
283 1
        return $clone;
284
    }
285
286
    /**
287
     * Produce a new set with enumerators in this but not in other (this - other)
288
     * @param EnumSet ...$other Other EnumSet(s) of the same enumeration to produce the union
289
     * @return EnumSet
290
     */
291 2 View Code Duplication
    public function diff(EnumSet $other)
0 ignored issues
show
Unused Code introduced by
The parameter $other is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
292
    {
293 2
        $bitset = '';
294 2
        foreach (func_get_args() as $other) {
295 2
            if (!$other instanceof self || $this->enumeration !== $other->enumeration) {
296 1
                throw new InvalidArgumentException(sprintf(
297 1
                    "Others should be an instance of %s of the same enumeration as this %s",
298 1
                    __CLASS__,
299 1
                    $this->enumeration
300 1
                ));
301
            }
302
303 1
            $bitset |= $other->bitset;
304 1
        }
305
306 1
        $clone = clone $this;
307 1
        $clone->bitset = $this->bitset & ~$bitset;
308 1
        return $clone;
309
    }
310
311
    /**
312
     * Produce a new set with enumerators in either this and other but not in both (this ^ (other | other))
313
     * @param EnumSet ...$other Other EnumSet(s) of the same enumeration to produce the union
314
     * @return EnumSet
315
     */
316 2 View Code Duplication
    public function symDiff(EnumSet $other)
0 ignored issues
show
Unused Code introduced by
The parameter $other is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317
    {
318 2
        $bitset = '';
319 2
        foreach (func_get_args() as $other) {
320 2
            if (!$other instanceof self || $this->enumeration !== $other->enumeration) {
321 1
                throw new InvalidArgumentException(sprintf(
322 1
                    "Others should be an instance of %s of the same enumeration as this %s",
323 1
                    __CLASS__,
324 1
                    $this->enumeration
325 1
                ));
326
            }
327
328 1
            $bitset |= $other->bitset;
329 1
        }
330
331 1
        $clone = clone $this;
332 1
        $clone->bitset = $this->bitset ^ $bitset;
333 1
        return $clone;
334
    }
335
336
    /**
337
     * Get ordinal numbers of the defined enumerators as array
338
     * @return int[]
339
     */
340 12
    public function getOrdinals()
341
    {
342 12
        $ordinals = array();
343 12
        $byteLen  = strlen($this->bitset);
344
345 12
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
346 12
            if ($this->bitset[$bytePos] === "\0") {
347 12
                continue; // fast skip null byte
348
            }
349
350 12
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
351 12
                if ((ord($this->bitset[$bytePos]) & (1 << $bitPos)) !== 0) {
352 12
                    $ordinals[] = $bytePos * 8 + $bitPos;
353 12
                }
354 12
            }
355 12
        }
356
357 12
        return $ordinals;
358
    }
359
360
    /**
361
     * Get values of the defined enumerators as array
362
     * @return null[]|bool[]|int[]|float[]|string[]
363
     */
364 6
    public function getValues()
365
    {
366 6
        $enumeration = $this->enumeration;
367 6
        $values      = array();
368 6
        foreach ($this->getOrdinals() as $ord) {
369 6
            $values[] = $enumeration::byOrdinal($ord)->getValue();
370 6
        }
371 6
        return $values;
372
    }
373
374
    /**
375
     * Get names of the defined enumerators as array
376
     * @return string[]
377
     */
378 2
    public function getNames()
379
    {
380 2
        $enumeration = $this->enumeration;
381 2
        $names       = array();
382 2
        foreach ($this->getOrdinals() as $ord) {
383 2
            $names[] = $enumeration::byOrdinal($ord)->getName();
384 2
        }
385 2
        return $names;
386
    }
387
388
    /**
389
     * Get the defined enumerators as array
390
     * @return Enum[]
391
     */
392 2
    public function getEnumerators()
393
    {
394 2
        $enumeration = $this->enumeration;
395 2
        $enumerators = array();
396 2
        foreach ($this->getOrdinals() as $ord) {
397 2
            $enumerators[] = $enumeration::byOrdinal($ord);
398 2
        }
399 2
        return $enumerators;
400
    }
401
402
    /**
403
     * Get binary bitset in little-endian order
404
     * 
405
     * @return string
406
     */
407 4
    public function getBinaryBitsetLe()
408
    {
409 4
        return $this->bitset;
410
    }
411
412
    /**
413
     * Set binary bitset in little-endian order
414
     *
415
     * NOTE: It resets the current position of the iterator
416
     * 
417
     * @param string $bitset
418
     * @return void
419
     * @throws InvalidArgumentException On a non string is given as Parameter
420
     */
421 8
    public function setBinaryBitsetLe($bitset)
422
    {
423 8
        if (!is_string($bitset)) {
424 1
            throw new InvalidArgumentException('Bitset must be a string');
425
        }
426
427 7
        $size   = strlen($this->bitset);
428 7
        $sizeIn = strlen($bitset);
429
430 7
        if ($sizeIn < $size) {
431
            // add "\0" if the given bitset is not long enough
432 1
            $bitset .= str_repeat("\0", $size - $sizeIn);
433 7
        } elseif ($sizeIn > $size) {
434 3
            if (trim(substr($bitset, $size), "\0") !== '') {
435 2
                throw new InvalidArgumentException('Out-Of-Range bits detected');
436
            }
437 1
            $bitset = substr($bitset, 0, $size);
438 1
        }
439
440
        // truncate out-of-range bits of last byte
441 5
        $lastByteMaxOrd = $this->ordinalMax % 8;
442 5
        if ($lastByteMaxOrd !== 0) {
443 4
            $lastByte         = $bitset[$size - 1];
444 4
            $lastByteExpected = chr((1 << $lastByteMaxOrd) - 1) & $lastByte;
445 4
            if ($lastByte !== $lastByteExpected) {
446 1
                throw new InvalidArgumentException('Out-Of-Range bits detected');
447
            }
448
449 3
            $this->bitset = substr($bitset, 0, -1) . $lastByteExpected;
450 3
        }
451
452 4
        $this->bitset = $bitset;
453
454
        // reset the iterator position
455 4
        $this->rewind();
456 4
    }
457
458
    /**
459
     * Get binary bitset in big-endian order
460
     * 
461
     * @return string
462
     */
463 1
    public function getBinaryBitsetBe()
464
    {
465 1
        return strrev($this->bitset);
466
    }
467
468
    /**
469
     * Set binary bitset in big-endian order
470
     *
471
     * NOTE: It resets the current position of the iterator
472
     * 
473
     * @param string $bitset
474
     * @return void
475
     * @throws InvalidArgumentException On a non string is given as Parameter
476
     */
477 2
    public function setBinaryBitsetBe($bitset)
478
    {
479 2
        if (!is_string($bitset)) {
480 1
            throw new InvalidArgumentException('Bitset must be a string');
481
        }
482 1
        $this->setBinaryBitsetLe(strrev($bitset));
483 1
    }
484
485
    /**
486
     * Get a bit at the given ordinal number
487
     * 
488
     * @param $ordinal int Ordinal number of bit to get
489
     * @return boolean
490
     */
491 19
    private function getBit($ordinal)
492
    {
493 19
        return (ord($this->bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
494
    }
495
496
    /**
497
     * Set a bit at the given ordinal number
498
     * 
499
     * @param $ordinal int Ordnal number of bit to set
500
     * @return void
501
     */
502 34
    private function setBit($ordinal)
503
    {
504 34
        $byte = (int) ($ordinal / 8);
505 34
        $this->bitset[$byte] = $this->bitset[$byte] | chr(1 << ($ordinal % 8));
506 34
    }
507
508
    /**
509
     * Unset a bit at the given ordinal number
510
     * 
511
     * @param $ordinal int Ordinal number of bit to unset
512
     * @return void
513
     */
514 6
    private function unsetBit($ordinal)
515
    {
516 6
        $byte = (int) ($ordinal / 8);
517 6
        $this->bitset[$byte] = $this->bitset[$byte] & chr(~(1 << ($ordinal % 8)));
518 6
    }
519
}
520