Skip to content

Commit 2205bbc

Browse files
committed
EnumSet implements IteratorAggregate instead of Iterator and return Generator
1 parent ee72c35 commit 2205bbc

File tree

4 files changed

+306
-463
lines changed

4 files changed

+306
-463
lines changed

src/Enum.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ final public static function byOrdinal(int $ordinal): self
249249

250250
if (!isset(self::$names[static::class][$ordinal])) {
251251
throw new InvalidArgumentException(\sprintf(
252-
'Invalid ordinal number, must between 0 and %s',
252+
'Invalid ordinal number %s, must between 0 and %s',
253+
$ordinal,
253254
\count(self::$names[static::class]) - 1
254255
));
255256
}

src/EnumSet.php

Lines changed: 26 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
namespace MabeEnum;
66

77
use Countable;
8-
use Iterator;
98
use InvalidArgumentException;
9+
use Iterator;
10+
use IteratorAggregate;
11+
use OutOfBoundsException;
1012

1113
/**
1214
* A set of enumerators of the given enumeration (EnumSet<T>)
@@ -16,7 +18,7 @@
1618
* @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
1719
* @link http://github.com/marc-mabe/php-enum for the canonical source repository
1820
*/
19-
class EnumSet implements Iterator, Countable
21+
class EnumSet implements IteratorAggregate, Countable
2022
{
2123
/**
2224
* The classname of the Enumeration
@@ -25,16 +27,10 @@ class EnumSet implements Iterator, Countable
2527
private $enumeration;
2628

2729
/**
28-
* Ordinal number of current iterator position
30+
* Number of enumerators defined in the enumeration
2931
* @var int
3032
*/
31-
private $ordinal = 0;
32-
33-
/**
34-
* Highest possible ordinal number
35-
* @var int
36-
*/
37-
private $ordinalMax;
33+
private $enumerationCount;
3834

3935
/**
4036
* Integer or binary (little endian) bitset
@@ -49,7 +45,6 @@ class EnumSet implements Iterator, Countable
4945
*
5046
* @var string
5147
*/
52-
private $fnDoRewind = 'doRewindInt';
5348
private $fnDoCount = 'doCountInt';
5449
private $fnDoGetOrdinals = 'doGetOrdinalsInt';
5550
private $fnDoGetBit = 'doGetBitInt';
@@ -75,18 +70,17 @@ public function __construct(string $enumeration)
7570
));
7671
}
7772

78-
$this->enumeration = $enumeration;
79-
$this->ordinalMax = \count($enumeration::getConstants());
73+
$this->enumeration = $enumeration;
74+
$this->enumerationCount = \count($enumeration::getConstants());
8075

8176
// By default the bitset is initialized as integer bitset
8277
// in case the enumeraton has more enumerators then integer bits
8378
// we will switch this into a binary bitset
84-
if ($this->ordinalMax > \PHP_INT_SIZE * 8) {
79+
if ($this->enumerationCount > \PHP_INT_SIZE * 8) {
8580
// init binary bitset with zeros
86-
$this->bitset = \str_repeat("\0", (int)\ceil($this->ordinalMax / 8));
81+
$this->bitset = \str_repeat("\0", (int)\ceil($this->enumerationCount / 8));
8782

8883
// switch internal binary bitset functions
89-
$this->fnDoRewind = 'doRewindBin';
9084
$this->fnDoCount = 'doCountBin';
9185
$this->fnDoGetOrdinals = 'doGetOrdinalsBin';
9286
$this->fnDoGetBit = 'doGetBitBin';
@@ -138,106 +132,22 @@ public function contains($enumerator): bool
138132
return $this->{$this->fnDoGetBit}(($this->enumeration)::get($enumerator)->getOrdinal());
139133
}
140134

141-
/* Iterator */
142-
143-
/**
144-
* Get the current enumerator
145-
* @return Enum|null Returns the current enumerator or NULL on an invalid iterator position
146-
*/
147-
public function current(): ?Enum
148-
{
149-
if ($this->valid()) {
150-
return ($this->enumeration)::byOrdinal($this->ordinal);
151-
}
152-
153-
return null;
154-
}
135+
/* IteratorAggregate */
155136

156137
/**
157-
* Get the ordinal number of the current iterator position
158-
* @return int
138+
* Create and return a new iterator
139+
* @return Iterator
159140
*/
160-
public function key(): int
141+
public function getIterator(): Iterator
161142
{
162-
return $this->ordinal;
163-
}
164-
165-
/**
166-
* Go to the next valid iterator position.
167-
* If no valid iterator position is found the iterator position will be the last possible + 1.
168-
* @return void
169-
*/
170-
public function next(): void
171-
{
172-
do {
173-
if (++$this->ordinal >= $this->ordinalMax) {
174-
$this->ordinal = $this->ordinalMax;
175-
return;
143+
$ordinal = -1;
144+
while (++$ordinal < $this->enumerationCount) {
145+
if ($this->{$this->fnDoGetBit}($ordinal)) {
146+
yield $ordinal => ($this->enumeration)::byOrdinal($ordinal);
176147
}
177-
} while (!$this->{$this->fnDoGetBit}($this->ordinal));
178-
}
179-
180-
/**
181-
* Go to the first valid iterator position.
182-
* If no valid iterator position was found the iterator position will be 0.
183-
* @return void
184-
* @uses doRewindBin()
185-
* @uses doRewindInt()
186-
*/
187-
public function rewind(): void
188-
{
189-
$this->{$this->fnDoRewind}();
190-
}
191-
192-
/**
193-
* Go to the first valid iterator position.
194-
* If no valid iterator position was found the iterator position will be 0.
195-
*
196-
* This is the binary bitset implementation.
197-
*
198-
* @return void
199-
* @see rewind()
200-
* @see doRewindInt()
201-
*/
202-
private function doRewindBin(): void
203-
{
204-
if (\ltrim($this->bitset, "\0") !== '') {
205-
$this->ordinal = -1;
206-
$this->next();
207-
} else {
208-
$this->ordinal = 0;
209148
}
210149
}
211150

212-
/**
213-
* Go to the first valid iterator position.
214-
* If no valid iterator position was found the iterator position will be 0.
215-
*
216-
* This is the binary bitset implementation.
217-
*
218-
* @return void
219-
* @see rewind()
220-
* @see doRewindBin()
221-
*/
222-
private function doRewindInt(): void
223-
{
224-
if ($this->bitset) {
225-
$this->ordinal = -1;
226-
$this->next();
227-
} else {
228-
$this->ordinal = 0;
229-
}
230-
}
231-
232-
/**
233-
* Test if the iterator is in a valid state
234-
* @return bool
235-
*/
236-
public function valid(): bool
237-
{
238-
return $this->ordinal !== $this->ordinalMax && $this->{$this->fnDoGetBit}($this->ordinal);
239-
}
240-
241151
/* Countable */
242152

243153
/**
@@ -500,7 +410,7 @@ private function doGetOrdinalsBin(): array
500410
private function doGetOrdinalsInt(): array
501411
{
502412
$ordinals = [];
503-
$ordinalMax = $this->ordinalMax;
413+
$ordinalMax = $this->enumerationCount;
504414
$bitset = $this->bitset;
505415
for ($ord = 0; $ord < $ordinalMax; ++$ord) {
506416
if ($bitset & (1 << $ord)) {
@@ -590,7 +500,7 @@ private function doGetBinaryBitsetLeBin(): string
590500
private function doGetBinaryBitsetLeInt(): string
591501
{
592502
$bin = \pack(\PHP_INT_SIZE === 8 ? 'P' : 'V', $this->bitset);
593-
return \substr($bin, 0, (int)\ceil($this->ordinalMax / 8));
503+
return \substr($bin, 0, (int)\ceil($this->enumerationCount / 8));
594504
}
595505

596506
/**
@@ -607,9 +517,6 @@ private function doGetBinaryBitsetLeInt(): string
607517
public function setBinaryBitsetLe(string $bitset): void
608518
{
609519
$this->{$this->fnDoSetBinaryBitsetLe}($bitset);
610-
611-
// reset the iterator position
612-
$this->rewind();
613520
}
614521

615522
/**
@@ -639,7 +546,7 @@ private function doSetBinaryBitsetLeBin(string $bitset): void
639546
}
640547

641548
// truncate out-of-range bits of last byte
642-
$lastByteMaxOrd = $this->ordinalMax % 8;
549+
$lastByteMaxOrd = $this->enumerationCount % 8;
643550
if ($lastByteMaxOrd !== 0) {
644551
$lastByte = $bitset[-1];
645552
$lastByteExpected = \chr((1 << $lastByteMaxOrd) - 1) & $lastByte;
@@ -678,7 +585,7 @@ private function doSetBinaryBitsetLeInt(string $bitset): void
678585
$int |= $ord << (8 * $i);
679586
}
680587

681-
if ($int & (~0 << $this->ordinalMax)) {
588+
if ($int & (~0 << $this->enumerationCount)) {
682589
throw new InvalidArgumentException('out-of-range bits detected');
683590
}
684591

@@ -720,8 +627,8 @@ public function setBinaryBitsetBe(string $bitset): void
720627
*/
721628
public function getBit(int $ordinal): bool
722629
{
723-
if ($ordinal < 0 || $ordinal > $this->ordinalMax) {
724-
throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->ordinalMax}");
630+
if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
631+
throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}");
725632
}
726633

727634
return $this->{$this->fnDoGetBit}($ordinal);
@@ -771,8 +678,8 @@ private function doGetBitInt(int $ordinal): bool
771678
*/
772679
public function setBit(int $ordinal, bool $bit): void
773680
{
774-
if ($ordinal < 0 || $ordinal > $this->ordinalMax) {
775-
throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->ordinalMax}");
681+
if ($ordinal < 0 || $ordinal > $this->enumerationCount) {
682+
throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}");
776683
}
777684

778685
if ($bit) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace MabeEnumTest;
4+
5+
use InvalidArgumentException;
6+
use MabeEnum\EnumSet;
7+
use MabeEnumTest\TestAsset\EmptyEnum;
8+
use MabeEnumTest\TestAsset\Enum31;
9+
use MabeEnumTest\TestAsset\EnumBasic;
10+
use MabeEnumTest\TestAsset\EnumInheritance;
11+
use MabeEnumTest\TestAsset\Enum32;
12+
use MabeEnumTest\TestAsset\Enum64;
13+
use MabeEnumTest\TestAsset\Enum65;
14+
use MabeEnumTest\TestAsset\Enum66;
15+
use OutOfBoundsException;
16+
use PHPUnit\Framework\TestCase;
17+
18+
/**
19+
* Unit tests for the class MabeEnum\EnumSet
20+
*
21+
* @copyright 2019 Marc Bennewitz
22+
* @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License
23+
* @link http://github.com/marc-mabe/php-enum for the canonical source repository
24+
*/
25+
class EnumSetIteratorTest extends TestCase
26+
{
27+
public function testIterateEmpty()
28+
{
29+
$set = new EnumSet(EnumBasic::class);
30+
31+
$this->assertSame([], iterator_to_array($set->getIterator()));
32+
}
33+
34+
public function testIterateOrdered()
35+
{
36+
$set = new EnumSet(EnumBasic::class);
37+
$set->attach(EnumBasic::FOUR);
38+
$set->attach(EnumBasic::TWO);
39+
$set->attach(EnumBasic::SEVEN);
40+
41+
$this->assertSame([
42+
1 => EnumBasic::TWO(),
43+
3 => EnumBasic::FOUR(),
44+
6 => EnumBasic::SEVEN(),
45+
], iterator_to_array($set));
46+
}
47+
48+
public function testMultipleIterators()
49+
{
50+
$set = new EnumSet(EnumBasic::class);
51+
$set->attach(EnumBasic::ONE);
52+
$set->attach(EnumBasic::TWO);
53+
54+
$it1 = $set->getIterator();
55+
$it2 = $set->getIterator();
56+
$it2->next();
57+
58+
$this->assertSame(0, $it1->key());
59+
$this->assertSame(1, $it2->key());
60+
}
61+
62+
public function testStartAtFirstValidPosition()
63+
{
64+
$set = new EnumSet(EnumBasic::class);
65+
$set->attach(EnumBasic::SEVEN);
66+
67+
$it = $set->getIterator();
68+
$this->assertSame(EnumBasic::SEVEN(), $it->current());
69+
$this->assertSame(EnumBasic::SEVEN()->getOrdinal(), $it->key());
70+
}
71+
72+
public function testCurrentOnEmpty()
73+
{
74+
$set = new EnumSet(EnumBasic::class);
75+
76+
$it = $set->getIterator();
77+
$this->assertFalse($it->valid());
78+
$this->assertNull($it->key());
79+
$this->assertNull($it->current());
80+
}
81+
82+
/**
83+
* @param string $enumeration
84+
* @dataProvider getIntegerEnumerations
85+
*/
86+
public function testNextCurrentOutOfRange(string $enumeration)
87+
{
88+
$set = new EnumSet($enumeration);
89+
$count = count($enumeration::getConstants());
90+
$last = $enumeration::byOrdinal($count - 1);
91+
$set->attach($last);
92+
93+
$it = $set->getIterator();
94+
$this->assertTrue($it->valid());
95+
$this->assertSame($last, $it->current());
96+
$this->assertSame($last->getOrdinal(), $it->key());
97+
98+
// go to the first out-of-range position
99+
$it->next();
100+
$this->assertFalse($it->valid());
101+
$this->assertNull($it->key());
102+
$this->assertNull($it->current());
103+
}
104+
105+
/**
106+
* Data provider for all available integer enumerators
107+
* @return array
108+
*/
109+
public function getIntegerEnumerations()
110+
{
111+
return [
112+
[Enum31::class],
113+
[Enum32::class],
114+
[Enum64::class],
115+
[Enum65::class],
116+
[Enum66::class]
117+
];
118+
}
119+
}

0 commit comments

Comments
 (0)