Completed
Push — master ( 6b5f05...900183 )
by Marc
02:35
created

EnumSet::union()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 12

Duplication

Lines 19
Ratio 100 %

Code Coverage

Tests 14
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 19
loc 19
ccs 14
cts 14
cp 1
rs 9.2
cc 4
eloc 12
nc 3
nop 1
crap 4
1
<?php
2
3
namespace MabeEnum;
4
5
use Iterator;
6
use Countable;
7
use InvalidArgumentException;
8
9
/**
10
 * EnumSet implementation in base of an integer bit set
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 51
    public function __construct($enumeration)
49
    {
50 51
        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 50
        $this->enumeration = $enumeration;
58 50
        $this->ordinalMax  = count($enumeration::getConstants());
59
        
60
        // init the bitset with zeros
61 50
        $this->bitset = str_repeat("\0", ceil($this->ordinalMax / 8));
62 50
    }
63
64
    /**
65
     * Get the classname of enumeration this set is for
66
     * @return string
67
     * @deprecated Please use getEnumeration() instead
68
     */
69
    public function getEnumClass()
70
    {
71
        return $this->getEnumeration();
72
    }
73
74
    /**
75
     * Get the classname of the enumeration
76
     * @return string
77
     */
78 12
    public function getEnumeration()
79
    {
80 12
        return $this->enumeration;
81
    }
82
83
    /**
84
     * Attach a new enumerator or overwrite an existing one
85
     * @param Enum|null|boolean|int|float|string $enumerator
86
     * @return void
87
     * @throws InvalidArgumentException On an invalid given enumerator
88
     */
89 36
    public function attach($enumerator)
90
    {
91 36
        $enumeration = $this->enumeration;
92 36
        $this->setBit($enumeration::get($enumerator)->getOrdinal());
93 36
    }
94
95
    /**
96
     * Detach the given enumerator
97
     * @param Enum|null|boolean|int|float|string $enumerator
98
     * @return void
99
     * @throws InvalidArgumentException On an invalid given enumerator
100
     */
101 7
    public function detach($enumerator)
102
    {
103 7
        $enumeration = $this->enumeration;
104 7
        $this->unsetBit($enumeration::get($enumerator)->getOrdinal());
105 7
    }
106
107
    /**
108
     * Test if the given enumerator was attached
109
     * @param Enum|null|boolean|int|float|string $enumerator
110
     * @return boolean
111
     */
112 11
    public function contains($enumerator)
113
    {
114 11
        $enumeration = $this->enumeration;
115 11
        return $this->getBit($enumeration::get($enumerator)->getOrdinal());
116
    }
117
118
    /* Iterator */
119
120
    /**
121
     * Get the current enumerator
122
     * @return Enum|null Returns the current enumerator or NULL on an invalid iterator position
123
     */
124 9
    public function current()
125
    {
126 9
        if ($this->valid()) {
127 9
            $enumeration = $this->enumeration;
128 9
            return $enumeration::getByOrdinal($this->ordinal);
129
        }
130
131 2
        return null;
132
    }
133
134
    /**
135
     * Get the ordinal number of the current iterator position
136
     * @return int
137
     */
138 6
    public function key()
139
    {
140 6
        return $this->ordinal;
141
    }
142
143
    /**
144
     * Go to the next valid iterator position.
145
     * If no valid iterator position is found the iterator position will be the last possible + 1.
146
     * @return void
147
     */
148 17
    public function next()
149
    {
150
        do {
151 17
            if (++$this->ordinal >= $this->ordinalMax) {
152 4
                $this->ordinal = $this->ordinalMax;
153 4
                return;
154
            }
155 17
        } while (!$this->getBit($this->ordinal));
156 17
    }
157
158
    /**
159
     * Go to the first valid iterator position.
160
     * If no valid iterator position in found the iterator position will be 0.
161
     * @return void
162
     */
163 11
    public function rewind()
164
    {
165 11
        if (trim($this->bitset, "\0") !== '') {
166 11
            $this->ordinal = -1;
167 11
            $this->next();
168 11
        } else {
169 1
            $this->ordinal = 0;
170
        }
171 11
    }
172
173
    /**
174
     * Test if the iterator in a valid state
175
     * @return boolean
176
     */
177 10
    public function valid()
178
    {
179 10
        return $this->ordinal !== $this->ordinalMax && $this->getBit($this->ordinal);
180
    }
181
182
    /* Countable */
183
184
    /**
185
     * Count the number of elements
186
     * @return int
187
     */
188 11
    public function count()
189
    {
190 11
        $count   = 0;
191 11
        $byteLen = strlen($this->bitset);
192 11
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
193 10
            if ($this->bitset[$bytePos] === "\0") {
194 8
                continue; // fast skip null byte
195
            }
196
197 10
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
198 10
                if ((ord($this->bitset[$bytePos]) & (1 << $bitPos)) !== 0) {
199 10
                    ++$count;
200 10
                }
201 10
            }
202 10
        }
203
204 11
        return $count;
205
    }
206
207
    /**
208
     * Check if this EnumSet is the same as other
209
     * @param EnumSet $other
210
     * @return bool
211
     */
212 2
    public function isEqual(EnumSet $other)
213
    {
214 2
        return $this->getEnumeration() === $other->getEnumeration()
215 2
            && $this->getBinaryBitsetLe() === $other->getBinaryBitsetLe();
216
    }
217
218
    /**
219
     * Check if this EnumSet is a subset of other
220
     * @param EnumSet $other
221
     * @return bool
222
     */
223 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...
224
    {
225 4
        if ($this->getEnumeration() !== $other->getEnumeration()) {
226 1
            return false;
227
        }
228
229 3
        $thisBitset = $this->getBinaryBitsetLe();
230 3
        return ($thisBitset & $other->getBinaryBitsetLe()) === $thisBitset;
231
    }
232
233
    /**
234
     * Check if this EnumSet is a superset of other
235
     * @param EnumSet $other
236
     * @return bool
237
     */
238 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...
239
    {
240 4
        if ($this->getEnumeration() !== $other->getEnumeration()) {
241 1
            return false;
242
        }
243
244 3
        $thisBitset = $this->getBinaryBitsetLe();
245 3
        return ($thisBitset | $other->getBinaryBitsetLe()) === $thisBitset;
246
    }
247
248
    /**
249
     * Produce a new set with enumerators from both this and other (this | other)
250
     * @param EnumSet ...$other Other EnumSet(s) of the same enumeration to produce the union
251
     * @return EnumSet
252
     */
253 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...
254
    {
255 2
        $bitset = $this->bitset;
256 2
        foreach (func_get_args() as $other) {
257 2
            if (!$other instanceof self || $this->enumeration !== $other->enumeration) {
258 1
                throw new InvalidArgumentException(sprintf(
259 1
                    "Others should be an instance of %s of the same enumeration as this %s",
260 1
                    __CLASS__,
261 1
                    $this->enumeration
262 1
                ));
263
            }
264
265 1
            $bitset |= $other->bitset;
266 1
        }
267
268 1
        $clone = clone $this;
269 1
        $clone->bitset = $bitset;
270 1
        return $clone;
271
    }
272
273
    /**
274
     * Produce a new set with enumerators common to both this and other (this & other)
275
     * @param EnumSet ...$other Other EnumSet(s) of the same enumeration to produce the union
276
     * @return EnumSet
277
     */
278 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...
279
    {
280 2
        $bitset = $this->bitset;
281 2
        foreach (func_get_args() as $other) {
282 2
            if (!$other instanceof self || $this->enumeration !== $other->enumeration) {
283 1
                throw new InvalidArgumentException(sprintf(
284 1
                    "Others should be an instance of %s of the same enumeration as this %s",
285 1
                    __CLASS__,
286 1
                    $this->enumeration
287 1
                ));
288
            }
289
290 1
            $bitset &= $other->bitset;
291 1
        }
292
293 1
        $clone = clone $this;
294 1
        $clone->bitset = $bitset;
295 1
        return $clone;
296
    }
297
298
    /**
299
     * Produce a new set with enumerators in this but not in other (this - other)
300
     * @param EnumSet ...$other Other EnumSet(s) of the same enumeration to produce the union
301
     * @return EnumSet
302
     */
303 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...
304
    {
305 2
        $bitset = '';
306 2
        foreach (func_get_args() as $other) {
307 2
            if (!$other instanceof self || $this->enumeration !== $other->enumeration) {
308 1
                throw new InvalidArgumentException(sprintf(
309 1
                    "Others should be an instance of %s of the same enumeration as this %s",
310 1
                    __CLASS__,
311 1
                    $this->enumeration
312 1
                ));
313
            }
314
315 1
            $bitset |= $other->bitset;
316 1
        }
317
318 1
        $clone = clone $this;
319 1
        $clone->bitset = $this->bitset & ~$bitset;
320 1
        return $clone;
321
    }
322
323
    /**
324
     * Produce a new set with enumerators in either this and other but not in both (this ^ (other | other))
325
     * @param EnumSet ...$other Other EnumSet(s) of the same enumeration to produce the union
326
     * @return EnumSet
327
     */
328 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...
329
    {
330 2
        $bitset = '';
331 2
        foreach (func_get_args() as $other) {
332 2
            if (!$other instanceof self || $this->enumeration !== $other->enumeration) {
333 1
                throw new InvalidArgumentException(sprintf(
334 1
                    "Others should be an instance of %s of the same enumeration as this %s",
335 1
                    __CLASS__,
336 1
                    $this->enumeration
337 1
                ));
338
            }
339
340 1
            $bitset |= $other->bitset;
341 1
        }
342
343 1
        $clone = clone $this;
344 1
        $clone->bitset = $this->bitset ^ $bitset;
345 1
        return $clone;
346
    }
347
348
    /**
349
     * Get ordinal numbers of the defined enumerators as array
350
     * @return int[]
351
     */
352 12
    public function getOrdinals()
353
    {
354 12
        $ordinals = array();
355 12
        $byteLen  = strlen($this->bitset);
356
357 12
        for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
358 12
            if ($this->bitset[$bytePos] === "\0") {
359 12
                continue; // fast skip null byte
360
            }
361
362 12
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
363 12
                if ((ord($this->bitset[$bytePos]) & (1 << $bitPos)) !== 0) {
364 12
                    $ordinals[] = $bytePos * 8 + $bitPos;
365 12
                }
366 12
            }
367 12
        }
368
369 12
        return $ordinals;
370
    }
371
372
    /**
373
     * Get values of the defined enumerators as array
374
     * @return null[]|bool[]|int[]|float[]|string[]
375
     */
376 6
    public function getValues()
377
    {
378 6
        $values = array();
379 6
        foreach ($this->getEnumerators() as $enumerator) {
380 6
            $values[] = $enumerator->getValue();
381 6
        }
382 6
        return $values;
383
    }
384
385
    /**
386
     * Get names of the defined enumerators as array
387
     * @return string[]
388
     */
389 2
    public function getNames()
390
    {
391 2
        $names = array();
392 2
        foreach ($this->getEnumerators() as $enumerator) {
393 2
            $names[] = $enumerator->getName();
394 2
        }
395 2
        return $names;
396
    }
397
398
    /**
399
     * Get the defined enumerators as array
400
     * @return Enum[]
401
     */
402 10
    public function getEnumerators()
403
    {
404 10
        $enumeration = $this->enumeration;
405 10
        $enumerators = array();
406 10
        foreach ($this->getOrdinals() as $ord) {
407 10
            $enumerators[] = $enumeration::getByOrdinal($ord);
408 10
        }
409 10
        return $enumerators;
410
    }
411
412
    /**
413
     * Get binary bitset in little-endian order
414
     * 
415
     * @return string
416
     */
417 11
    public function getBinaryBitsetLe()
418
    {
419 11
        return $this->bitset;
420
    }
421
422
    /**
423
     * Set binary bitset in little-endian order
424
     *
425
     * NOTE: It resets the current position of the iterator
426
     * 
427
     * @param string $bitset
428
     * @return void
429
     * @throws InvalidArgumentException On a non string is given as Parameter
430
     */
431 7
    public function setBinaryBitsetLe($bitset)
432
    {
433 7
        if (!is_string($bitset)) {
434 1
            throw new InvalidArgumentException('Bitset must be a string');
435
        }
436
437 6
        $size   = strlen($this->bitset);
438 6
        $sizeIn = strlen($bitset);
439
440 6
        if ($sizeIn < $size) {
441
            // add "\0" if the given bitset is not long enough
442 1
            $bitset .= str_repeat("\0", $size - $sizeIn);
443 6
        } elseif ($sizeIn > $size) {
444 2
            $bitset = substr($bitset, 0, $size);
445 2
        }
446
447
        // truncate out-of-range bits of last byte
448 6
        $lastByteMaxOrd = $this->ordinalMax % 8;
449 6
        if ($lastByteMaxOrd === 0) {
450 1
            $this->bitset = $bitset;
451 1
        } else {
452 5
            $lastByte     = chr($lastByteMaxOrd) & $bitset[$size - 1];
453 5
            $this->bitset = substr($bitset, 0, -1) . $lastByte;
454
        }
455
456
        // reset the iterator position
457 6
        $this->rewind();
458 6
    }
459
460
    /**
461
     * Get binary bitset in big-endian order
462
     * 
463
     * @return string
464
     */
465 2
    public function getBinaryBitsetBe()
466
    {
467 2
        return strrev($this->bitset);
468
    }
469
470
    /**
471
     * Set binary bitset in big-endian order
472
     *
473
     * NOTE: It resets the current position of the iterator
474
     * 
475
     * @param string $bitset
476
     * @return void
477
     * @throws InvalidArgumentException On a non string is given as Parameter
478
     */
479 3
    public function setBinaryBitsetBe($bitset)
480
    {
481 3
        if (!is_string($bitset)) {
482 1
            throw new InvalidArgumentException('Bitset must be a string');
483
        }
484
        
485 2
	$this->setBinaryBitsetLe(strrev($bitset));
486 2
    }
487
488
    /**
489
     * Get the binary bitset
490
     * 
491
     * @return string Returns the binary bitset in big-endian order
492
     * @deprecated Please use getBinaryBitsetBe() instead
493
     */
494
    public function getBitset()
495
    {
496
        return $this->getBinaryBitsetBe();
497
    }
498
499
    /**
500
     * Set the bitset.
501
     * NOTE: It resets the current position of the iterator
502
     * 
503
     * @param string $bitset The binary bitset in big-endian order
504
     * @return void
505
     * @throws InvalidArgumentException On a non string is given as Parameter
506
     * @deprecated Please use setBinaryBitsetBe() instead
507
     */
508
    public function setBitset($bitset)
509
    {
510
        $this->setBinaryBitsetBE($bitset);
511
    }
512
513
    /**
514
     * Get a bit at the given ordinal number
515
     * 
516
     * @param $ordinal int Ordinal number of bit to get
517
     * @return boolean
518
     */
519 22
    private function getBit($ordinal)
520
    {
521 22
        return (ord($this->bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
522
    }
523
524
    /**
525
     * Set a bit at the given ordinal number
526
     * 
527
     * @param $ordinal int Ordnal number of bit to set
528
     * @return void
529
     */
530 36
    private function setBit($ordinal)
531
    {
532 36
        $byte = (int) ($ordinal / 8);
533 36
        $this->bitset[$byte] = $this->bitset[$byte] | chr(1 << ($ordinal % 8));
534 36
    }
535
536
    /**
537
     * Unset a bit at the given ordinal number
538
     * 
539
     * @param $ordinal int Ordinal number of bit to unset
540
     * @return void
541
     */
542 7
    private function unsetBit($ordinal)
543
    {
544 7
        $byte = (int) ($ordinal / 8);
545 7
        $this->bitset[$byte] = $this->bitset[$byte] & chr(~(1 << ($ordinal % 8)));
546 7
    }
547
}
548