Completed
Push — enumset_optimize_count ( e0df63 )
by Marc
02:03
created

EnumSet::count()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 18
ccs 13
cts 13
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 42
    public function __construct($enumeration)
49
    {
50 42 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 41
        $this->enumeration = $enumeration;
58 41
        $this->ordinalMax  = count($enumeration::getConstants());
59
        
60
        // init the bitset with zeros
61 41
        $this->bitset = str_repeat("\0", ceil($this->ordinalMax / 8));
62 41
    }
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 31
    public function attach($enumerator)
90
    {
91 31
        $enumeration = $this->enumeration;
92 31
        $this->setBit($enumeration::get($enumerator)->getOrdinal());
93 31
    }
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 16
    public function next()
149
    {
150
        do {
151 16
            if (++$this->ordinal >= $this->ordinalMax) {
152 4
                $this->ordinal = $this->ordinalMax;
153 4
                return;
154
            }
155 16
        } while (!$this->getBit($this->ordinal));
156 16
    }
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 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
        }
0 ignored issues
show
Coding Style introduced by
Closing brace indented incorrectly; expected 4 spaces, found 8
Loading history...
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
     * Get ordinal numbers of the defined enumerators as array
250
     * @return int[]
251
     */
252 8
    public function getOrdinals()
253
    {
254 8
        $ordinals = array();
255 8
        $byteLen  = strlen($this->bitset);
256
257 8
	for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) {
258 8
            if ($this->bitset[$bytePos] === "\0") {
259 8
                continue; // fast skip null byte
260
            }
261
262 8
            for ($bitPos = 0; $bitPos < 8; ++$bitPos) {
263 8
                if ((ord($this->bitset[$bytePos]) & (1 << $bitPos)) !== 0) {
264 8
                    $ordinals[] = $bytePos * 8 + $bitPos;
265 8
                }
266 8
            }
267 8
        }
0 ignored issues
show
Coding Style introduced by
Closing brace indented incorrectly; expected 4 spaces, found 8
Loading history...
268
269 8
        return $ordinals;
270
    }
271
272
    /**
273
     * Get values of the defined enumerators as array
274
     * @return null[]|bool[]|int[]|float[]|string[]
275
     */
276 2
    public function getValues()
277
    {
278 2
        $values = array();
279 2
        foreach ($this->getEnumerators() as $enumerator) {
280 2
            $values[] = $enumerator->getValue();
281 2
        }
282 2
        return $values;
283
    }
284
285
    /**
286
     * Get names of the defined enumerators as array
287
     * @return string[]
288
     */
289 2
    public function getNames()
290
    {
291 2
        $names = array();
292 2
        foreach ($this->getEnumerators() as $enumerator) {
293 2
            $names[] = $enumerator->getName();
294 2
        }
295 2
        return $names;
296
    }
297
298
    /**
299
     * Get the defined enumerators as array
300
     * @return Enum[]
301
     */
302 6
    public function getEnumerators()
303
    {
304 6
        $enumeration = $this->enumeration;
305 6
        $enumerators = array();
306 6
        foreach ($this->getOrdinals() as $ord) {
307 6
            $enumerators[] = $enumeration::getByOrdinal($ord);
308 6
        }
309 6
        return $enumerators;
310
    }
311
312
    /**
313
     * Get binary bitset in little-endian order
314
     * 
315
     * @return string
316
     */
317 10
    public function getBinaryBitsetLe()
318
    {
319 10
        return $this->bitset;
320
    }
321
322
    /**
323
     * Set binary bitset in little-endian order
324
     *
325
     * NOTE: It resets the current position of the iterator
326
     * 
327
     * @param string $bitset
328
     * @return void
329
     * @throws InvalidArgumentException On a non string is given as Parameter
330
     */
331 6
    public function setBinaryBitsetLe($bitset)
332
    {
333 6
        if (!is_string($bitset)) {
334 1
            throw new InvalidArgumentException('Bitset must be a string');
335
        }
336
        
337 5
        $size   = ceil($this->ordinalMax / 8);
338 5
        $sizeIn = strlen($bitset);
339
        
340 5
        if ($sizeIn < $size) {
341
            // add "\0" if the given bitset is not long enough
342 1
            $bitset .= str_repeat("\0", $size - $sizeIn);
343 5
        } elseif ($sizeIn > $size) {
344 1
            $bitset = substr($bitset, 0, $size);
345 1
        }
346
        
347 5
        $this->bitset = $bitset;
348
        
349 5
        $this->rewind();
350 5
    }
351
352
    /**
353
     * Get binary bitset in big-endian order
354
     * 
355
     * @return string
356
     */
357 2
    public function getBinaryBitsetBe()
358
    {
359 2
        return strrev($this->bitset);
360
    }
361
362
    /**
363
     * Set binary bitset in big-endian order
364
     *
365
     * NOTE: It resets the current position of the iterator
366
     * 
367
     * @param string $bitset
368
     * @return void
369
     * @throws InvalidArgumentException On a non string is given as Parameter
370
     */
371 3
    public function setBinaryBitsetBe($bitset)
372
    {
373 3
        if (!is_string($bitset)) {
374 1
            throw new InvalidArgumentException('Bitset must be a string');
375
        }
376
        
377 2
	$this->setBinaryBitsetLe(strrev($bitset));
378 2
    }
379
380
    /**
381
     * Get the binary bitset
382
     * 
383
     * @return string Returns the binary bitset in big-endian order
384
     * @deprecated Please use getBinaryBitsetBe() instead
385
     */
386
    public function getBitset()
387
    {
388
        return $this->getBinaryBitsetBe();
389
    }
390
391
    /**
392
     * Set the bitset.
393
     * NOTE: It resets the current position of the iterator
394
     * 
395
     * @param string $bitset The binary bitset in big-endian order
396
     * @return void
397
     * @throws InvalidArgumentException On a non string is given as Parameter
398
     * @deprecated Please use setBinaryBitsetBe() instead
399
     */
400
    public function setBitset($bitset)
401
    {
402
        $this->setBinaryBitsetBE($bitset);
403
    }
404
405
    /**
406
     * Get a bit at the given ordinal number
407
     * 
408
     * @param $ordinal int Ordinal number of bit to get
409
     * @return boolean
410
     */
411 21
    private function getBit($ordinal)
412
    {
413 21
        return (ord($this->bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0;
414
    }
415
416
    /**
417
     * Set a bit at the given ordinal number
418
     * 
419
     * @param $ordinal int Ordnal number of bit to set
420
     * @return void
421
     */
422 31
    private function setBit($ordinal)
423
    {
424 31
        $byte = (int) ($ordinal / 8);
425 31
        $this->bitset[$byte] = $this->bitset[$byte] | chr(1 << ($ordinal % 8));
426 31
    }
427
428
    /**
429
     * Unset a bit at the given ordinal number
430
     * 
431
     * @param $ordinal int Ordinal number of bit to unset
432
     * @return void
433
     */
434 7
    private function unsetBit($ordinal)
435
    {
436 7
        $byte = (int) ($ordinal / 8);
437 7
        $this->bitset[$byte] = $this->bitset[$byte] & chr(~(1 << ($ordinal % 8)));
438 7
    }
439
}
440