Completed
Pull Request — master (#65)
by Marc
04:25 queued 02:14
created

EnumSet::getOrdinals()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 19
ccs 5
cts 5
cp 1
rs 8.8571
cc 5
eloc 10
nc 3
nop 0
crap 5
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 34
    public function __construct($enumeration)
49
    {
50 34
        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 33
        $this->enumeration = $enumeration;
58 33
        $this->ordinalMax  = count($enumeration::getConstants());
59
        
60
        // init the bitset with zeros
61 33
        $this->bitset = str_repeat("\0", ceil($this->ordinalMax / 8));
62 33
    }
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 23
    public function attach($enumerator)
90
    {
91 23
        $enumeration = $this->enumeration;
92 23
        $this->setBit($enumeration::get($enumerator)->getOrdinal());
93 23
    }
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 5
    public function current()
125
    {
126 5
        if ($this->valid()) {
127 5
            $enumeration = $this->enumeration;
128 5
            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 12
    public function next()
149
    {
150
        do {
151 12
            if (++$this->ordinal >= $this->ordinalMax) {
152 4
                $this->ordinal = $this->ordinalMax;
153 4
                return;
154
            }
155 12
        } while (!$this->getBit($this->ordinal));
156 12
    }
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 10
    public function rewind()
164
    {
165 10
        if (trim($this->bitset, "\0") !== '') {
166 10
            $this->ordinal = -1;
167 10
            $this->next();
168 10
        } else {
169 1
            $this->ordinal = 0;
170
        }
171 10
    }
172
173
    /**
174
     * Test if the iterator in a valid state
175
     * @return boolean
176
     */
177 6
    public function valid()
178
    {
179 6
        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
        $cnt = 0;
191 11
        $ord = 0;
192
193 11
        while ($ord !== $this->ordinalMax) {
194 10
            if ($this->getBit($ord++)) {
195 10
                ++$cnt;
196 10
            }
197 10
        }
198
199 11
        return $cnt;
200
    }
201
202
    /**
203
     * Check if this EnumSet is the same as other
204
     * @param EnumSet $other
205
     * @return bool
206
     */
207 2
    public function isEqual(EnumSet $other)
208
    {
209 2
        return $this->getEnumeration() === $other->getEnumeration()
210 2
            && $this->getBinaryBitsetLe() === $other->getBinaryBitsetLe();
211
    }
212
213
    /**
214
     * Check if this EnumSet is a subset of other
215
     * @param EnumSet $other
216
     * @return bool
217
     */
218 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...
219
    {
220 4
        if ($this->getEnumeration() !== $other->getEnumeration()) {
221 1
            return false;
222
        }
223
224 3
        $thisBitset = $this->getBinaryBitsetLe();
225 3
        return ($thisBitset & $other->getBinaryBitsetLe()) === $thisBitset;
226
    }
227
228
    /**
229
     * Check if this EnumSet is a superset of other
230
     * @param EnumSet $other
231
     * @return bool
232
     */
233 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...
234
    {
235 4
        if ($this->getEnumeration() !== $other->getEnumeration()) {
236 1
            return false;
237
        }
238
239 3
        $thisBitset = $this->getBinaryBitsetLe();
240 3
        return ($thisBitset | $other->getBinaryBitsetLe()) === $thisBitset;
241
    }
242
243
    /**
244
     * Get ordinal numbers of the defined enumerators as array
245
     * @return int[]
246
     */
247
    public function getOrdinals()
248 10
    {
249
        $ordinals = array();
250 10
        $byteLen  = strlen($this->bitset);
251
252
	for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
253
            if ($this->bitset[$bytePos] === "\0") {
254
                continue; // fast skip null byte
255
            }
256
257
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
258
                if ((ord($this->bitset[$bytePos]) & 1 << $bitPos) !== 0) {
259
                    $ordinals[] = $bytePos * 8 + $bitPos;
260
                }
261
            }
262 6
        }
0 ignored issues
show
Coding Style introduced by
Closing brace indented incorrectly; expected 4 spaces, found 8
Loading history...
263
264 6
        return $ordinals;
265 1
    }
266
267
    /**
268 5
     * Get values of the defined enumerators as array
269 5
     * @return null[]|bool[]|int[]|float[]|string[]
270
     */
271 5
    public function getValues()
272
    {
273 1
        $values = array();
274 5
        foreach ($this->getEnumerators() as $enumerator) {
275 1
            $values[] = $enumerator->getValue();
276 1
        }
277
        return $values;
278 5
    }
279
280 5
    /**
281 5
     * Get names of the defined enumerators as array
282
     * @return string[]
283
     */
284
    public function getNames()
285
    {
286
        $names = array();
287
        foreach ($this->getEnumerators() as $enumerator) {
288 2
            $names[] = $enumerator->getName();
289
        }
290 2
        return $names;
291
    }
292
293
    /**
294
     * Get the defined enumerators as array
295
     * @return Enum[]
296
     */
297
    public function getEnumerators()
298
    {
299
        $enumeration = $this->enumeration;
300
        $enumerators = array();
301
        foreach ($this->getOrdinals() as $ord) {
302 3
            $enumerators[] = $enumeration::getByOrdinal($ord);
303
        }
304 3
        return $enumerators;
305 1
    }
306
307
    /**
308 2
     * Get binary bitset in little-endian order
309 2
     * 
310
     * @return string
311
     */
312
    public function getBinaryBitsetLe()
313
    {
314
        return $this->bitset;
315
    }
316
317
    /**
318
     * Set binary bitset in little-endian order
319
     *
320
     * NOTE: It resets the current position of the iterator
321
     * 
322
     * @param string $bitset
323
     * @return void
324
     * @throws InvalidArgumentException On a non string is given as Parameter
325
     */
326
    public function setBinaryBitsetLe($bitset)
327
    {
328
        if (!is_string($bitset)) {
329
            throw new InvalidArgumentException('Bitset must be a string');
330
        }
331
        
332
        $size   = ceil($this->ordinalMax / 8);
333
        $sizeIn = strlen($bitset);
334
        
335
        if ($sizeIn < $size) {
336
            // add "\0" if the given bitset is not long enough
337
            $bitset .= str_repeat("\0", $size - $sizeIn);
338
        } elseif ($sizeIn > $size) {
339
            $bitset = substr($bitset, 0, $size);
340
        }
341
        
342 18
        $this->bitset = $bitset;
343
        
344 18
        $this->rewind();
345
    }
346
347
    /**
348
     * Get binary bitset in big-endian order
349
     * 
350
     * @return string
351
     */
352
    public function getBinaryBitsetBe()
353 23
    {
354
        return strrev($this->bitset);
355 23
    }
356 23
357 23
    /**
358
     * Set binary bitset in big-endian order
359
     *
360
     * NOTE: It resets the current position of the iterator
361
     * 
362
     * @param string $bitset
363
     * @return void
364
     * @throws InvalidArgumentException On a non string is given as Parameter
365 7
     */
366
    public function setBinaryBitsetBe($bitset)
367 7
    {
368 7
        if (!is_string($bitset)) {
369 7
            throw new InvalidArgumentException('Bitset must be a string');
370
        }
371
        
372
	$this->setBinaryBitsetLe(strrev($bitset));
373
    }
374
375
    /**
376
     * Get the binary bitset
377
     * 
378
     * @return string Returns the binary bitset in big-endian order
379
     * @deprecated Please use getBinaryBitsetBe() instead
380
     */
381
    public function getBitset()
382
    {
383
        return $this->getBinaryBitsetBe();
384
    }
385
386
    /**
387
     * Set the bitset.
388
     * NOTE: It resets the current position of the iterator
389
     * 
390
     * @param string $bitset The binary bitset in big-endian order
391
     * @return void
392
     * @throws InvalidArgumentException On a non string is given as Parameter
393
     * @deprecated Please use setBinaryBitsetBe() instead
394
     */
395
    public function setBitset($bitset)
396
    {
397
        $this->setBinaryBitsetBE($bitset);
398
    }
399
400
    /**
401
     * Get a bit at the given ordinal number
402
     * 
403
     * @param $ordinal int Ordinal number of bit to get
404
     * @return boolean
405
     */
406
    private function getBit($ordinal)
407
    {
408
        return (ord($this->bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
409
    }
410
411
    /**
412
     * Set a bit at the given ordinal number
413
     * 
414
     * @param $ordinal int Ordnal number of bit to set
415
     * @return void
416
     */
417
    private function setBit($ordinal)
418
    {
419
        $byte = (int) ($ordinal / 8);
420
        $this->bitset[$byte] = $this->bitset[$byte] | chr(1 << ($ordinal % 8));
421
    }
422
423
    /**
424
     * Unset a bit at the given ordinal number
425
     * 
426
     * @param $ordinal int Ordinal number of bit to unset
427
     * @return void
428
     */
429
    private function unsetBit($ordinal)
430
    {
431
        $byte = (int) ($ordinal / 8);
432
        $this->bitset[$byte] = $this->bitset[$byte] & chr(~(1 << ($ordinal % 8)));
433
    }
434
}
435