diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 86eebca..ab70316 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest] - php-version: ['7.4', '8.0', '8.1', '8.2'] + php-version: ['8.1', '8.2'] name: PHP ${{ matrix.php-version }} steps: - name: Checkout @@ -29,17 +29,7 @@ jobs: - name: Install package run: | composer install - - name: Phpstan rules for PHP ${{ matrix.php-version }} - if: ${{ matrix.php-version == '7.4' || matrix.php-version == '8.0' }} - run: cp phpstan_below_81.neon phpstan.neon - - name: Main checks + - name: CI pack run: | - composer phpcs - composer phpstan - composer phpunit - - name: Infection and coverage - if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' }} - run: | - composer infection - composer coverage + composer ci:pack bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index f57b0ba..8d441cc 100644 --- a/README.md +++ b/README.md @@ -13,29 +13,30 @@ define('READ', 1 << 0); define('WRITE', 1 << 1); define('EXECUTE', 1 << 2); $mask = READ | WRITE | EXECUTE; -// read: 1 write: 2 execute: 4 mask: 7 echo sprintf('read: %d write: %d execute: %d mask: %d', READ, WRITE, EXECUTE, $mask); +// read: 1 write: 2 execute: 4 mask: 7 if ($mask & READ) { // $mask have a READ } ``` But you can try other way with this package: + ```php use BitMask\BitMask; use BitMask\Util\Bits; -$bitmask = new BitMask(); +$bitmask = new BitMask(0, 2); // no bits set, but only three bits allowed: 1, 2, 4 $bitmask->set(0b111); // 7, 1 << 0 | 1 << 1 | 1 << 2 // get value and check if single bit or mask is set $integerMask = $bitmask->get(); // int 7 -$boolIsSetBit = $bitmask->isSetBit(4); // bool true -$boolIsSetBit = $bitmask->isSetBitByShiftOffset(2); // true -$boolIsSetMask = $bitmask->isSet(6); // bool true +$boolIsSetBit = $bitmask->isSetBits(1, 2, 4); // bool true, variadic arguments +$boolIsSetBit = $bitmask->isSetBitByShiftOffset(2); // bool true, for single MSB +$boolIsSetMask = $bitmask->isSet(6); // bool true, single mask // get some info about bits -$integerMostSignificantBit = Bits::getMostSignificantBit($bitmask->get()); // int 3 +$integerMostSignificantBit = Bits::getMostSignificantBit($bitmask->get()); // int 7 $arraySetBits = Bits::getSetBits($bitmask->get()); // array:3 [1, 2, 4] $arraySetBitsIndexes = Bits::getSetBitsIndexes($bitmask->get()); // array:3 [0, 1, 2] $string = Bits::toString($bitmask->get()); // string "111" @@ -46,49 +47,15 @@ $integerIndex = Bits::bitToIndex(65536); // int 16 $boolIsSingleBit = Bits::isSingleBit(8); // true // change mask -$bitmask->unsetBit(4); -$bitmask->unsetBitByShiftOffset(2); -$bitmask->setBit(8); +$bitmask->unsetBits(4); // or $bitmask->unsetBitByShiftOffset(2); +Bits::getSetBits($bitmask->get()); // array:3 [1, 2] -Bits::getSetBits($bitmask->get()); // array:3 [1, 2, 8] +$bitmask->setBits(0b1000); // throws OutOfRangeException ``` Some examples can be found in [BitMaskInterface](/src/BitMaskInterface.php) and in [tests](/tests) -Exists `IndexedBitMask` and `AssociativeBitMask` helper classes: -```php -use BitMask\IndexedBitMask; -use BitMask\AssociativeBitMask; - -// Indexed are extended BitMask with one extra method: getByIndex -// For instance, mask 0b110 would have following "index:value": 0:false, 1:true, 2:true -// Indexes are RTL, starts from 0. Equals to mask left shift offset. -$indexed = new IndexedBitMask(1 << 1 | 1 << 2); // 0b110 -$indexed->getByIndex(2); // true -$indexed->getByIndex(0); // false - -// Associative are extended Indexed. In addition to the mask you must also specify the number of bits and the array of key strings. -// Each key will have a bitmask property with the same name and a method named 'is{Key}'. -$bitmask = new AssociativeBitMask(5, 3, ['readable', 'writable', 'executable']); // -$bitmask->getByKey('readable'); // bool(true) -/** __call */ -$boolReadable = $bitmask->isReadable(); // bool(true) -$boolWritable = $bitmask->isWritable(); // bool(true) -$boolExecutable = $bitmask->isExecutable(); // bool(true) -$result = $bitmask->isUnknownKey(); // BitMask\Exception\UnknownKeyException -/** __get */ -$boolReadable = $bitmask->readable; // bool true -$boolWritable = $bitmask->writable; // bool false -$boolExecutable = $bitmask->executable; // bool true -$result = $bitmask->unknownKey; // BitMask\Exception\UnknownKeyException -/** __set */ -$bitmask->readable = false; -$bitmask->writable = true; -$bitmask->executable = false; -$bitmask->unknownKey = true; // BitMask\Exception\UnknownKeyException -``` - -With PHP ^8.1 `EnumBitMask` can be used: +`EnumBitMask` is extended BitMask ```php enum Permissions { @@ -99,20 +66,13 @@ enum Permissions { use BitMask\EnumBitMask; -// First argument is required and expects FQCN of the enum -// Second argument is a variadic UnitEnum, and acts like setter of the bits -$bitmask = new EnumBitMask(Permissions::class, Permissions::Read, Permissions::Execute); -// previous statement is equals to following lines: -// $bitmask = new EnumBitMask(Permissions::class); -// $bitmask->set(Permissions::Read, Permissions::Execute); // set, isSet and unset also have variadic args - -$bitmask->isSet(Permissions::Read); // true -$bitmask->isSet(Permissions::Write); // false -$bitmask->isSet(Permissions::Execute); // true -$bitmask->set(Permissions::Write); -$bitmask->isSet(Permissions::Write, Permissions::Read); // true -$bitmask->unset(Permissions::Write); -$bitmask->isSet(Permissions::Write); // false +$bitmask = new EnumBitMask(Permissions::class); +$bitmask->setEnumBits(Permissions::Read, Permissions::Execute); +$bitmask->isSetEnumBits(Permissions::Read); // true +$bitmask->isSetEnumBits(Permissions::Write); // false +$bitmask->setEnumBits(Permissions::Write); +$bitmask->isSetEnumBits(Permissions::Read, Permissions::Write, Permissions::Execute); // true +$bitmask->unsetEnumBits(Permissions::Write); ``` ## Installing @@ -146,7 +106,7 @@ $ ./vendor/bin/phpbench run benchmarks --report=default ##### PHPStan ```bash $ composer phpstan -$ ./vendor/bin/phpstan analyse src/ -c phpstan.neon --level=8 --no-progress -vvv --memory-limit=1024M +$ ./vendor/bin/phpstan analyse src/ -c phpstan.neon --level=9 --no-progress -vvv --memory-limit=1024M ``` ##### PHP-CS ###### Code style check diff --git a/benchmarks/EvenOddBench.php b/benchmarks/EvenOddBench.php index 03f7288..9e60834 100644 --- a/benchmarks/EvenOddBench.php +++ b/benchmarks/EvenOddBench.php @@ -5,8 +5,8 @@ namespace Yaroslavche\Benchmarks; use Generator; -use PhpBench\Benchmark\Metadata\Annotations\ParamProviders; -use PhpBench\Benchmark\Metadata\Annotations\Revs; +use PhpBench\Attributes\ParamProviders; +use PhpBench\Attributes\Revs; class EvenOddBench { @@ -18,20 +18,18 @@ public function numberProvider(): Generator yield [PHP_INT_MAX]; } - /** - * @Revs(1000000) - * @ParamProviders({"numberProvider"}) - */ + /** @param int[] $number */ + #[Revs(1000000)] + #[ParamProviders('numberProvider')] public function benchEvenOdd1(array $number): void { $this->isEven1($number[0]); $this->isOdd1($number[0]); } - /** - * @Revs(1000000) - * @ParamProviders({"numberProvider"}) - */ + /** @param int[] $number */ + #[Revs(1000000)] + #[ParamProviders('numberProvider')] public function benchEvenOdd2(array $number): void { $this->isEven2($number[0]); diff --git a/benchmarks/GetSetBitsIndexBench.php b/benchmarks/GetSetBitsIndexBench.php index 36a0e93..b28c42d 100644 --- a/benchmarks/GetSetBitsIndexBench.php +++ b/benchmarks/GetSetBitsIndexBench.php @@ -5,9 +5,9 @@ use BitMask\Util\Bits as BitUtils; use Generator; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\ParamProviders; -use PhpBench\Benchmark\Metadata\Annotations\Revs; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\ParamProviders; +use PhpBench\Attributes\Revs; class GetSetBitsIndexBench { @@ -19,21 +19,19 @@ public function maskProvider(): Generator yield [1 << 32]; } - /** - * @Revs(100000) - * @Iterations(5) - * @ParamProviders({"maskProvider"}) - */ + /** @param int[] $mask */ + #[Revs(100000)] + #[Iterations(5)] + #[ParamProviders('maskProvider')] public function benchGetSetBitsIndex1(array $mask): void { $this->getSetBitsIndex1($mask[0]); } - /** - * @Revs(10000) - * @Iterations(5) - * @ParamProviders({"maskProvider"}) - */ + /** @param int[] $mask */ + #[Revs(100000)] + #[Iterations(5)] + #[ParamProviders('maskProvider')] public function benchGetSetBitsIndex2(array $mask): void { $this->getSetBitsIndex2($mask[0]); @@ -56,7 +54,7 @@ private function getSetBitsIndex2(int $mask): array { $bitIndexes = []; foreach (BitUtils::getSetBits($mask) as $index => $bit) { - $bitIndexes[$index] = (int)log($bit, 2); + $bitIndexes[$index] = BitUtils::getMostSignificantBit($bit); } return $bitIndexes; } diff --git a/benchmarks/IndexToBitBench.php b/benchmarks/IndexToBitBench.php index 69498a3..1fdd009 100644 --- a/benchmarks/IndexToBitBench.php +++ b/benchmarks/IndexToBitBench.php @@ -5,9 +5,9 @@ namespace Yaroslavche\Benchmarks; use Generator; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\ParamProviders; -use PhpBench\Benchmark\Metadata\Annotations\Revs; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\ParamProviders; +use PhpBench\Attributes\Revs; class IndexToBitBench { @@ -18,21 +18,19 @@ public function indexProvider(): Generator yield [10000]; } - /** - * @Revs(100000) - * @Iterations(5) - * @ParamProviders({"indexProvider"}) - */ + /** @param int[] $index */ + #[Revs(100000)] + #[Iterations(5)] + #[ParamProviders('indexProvider')] public function benchIndexToBit1(array $index): void { $this->indexToBit1($index[0]); } - /** - * @Revs(100000) - * @Iterations(5) - * @ParamProviders({"indexProvider"}) - */ + /** @param int[] $index */ + #[Revs(100000)] + #[Iterations(5)] + #[ParamProviders('indexProvider')] public function benchIndexToBit2(array $index): void { $this->indexToBit2($index[0]); diff --git a/benchmarks/IsSingleBitBench.php b/benchmarks/IsSingleBitBench.php index 9e86bde..4733490 100644 --- a/benchmarks/IsSingleBitBench.php +++ b/benchmarks/IsSingleBitBench.php @@ -6,9 +6,9 @@ use BitMask\Util\Bits; use Generator; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\ParamProviders; -use PhpBench\Benchmark\Metadata\Annotations\Revs; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\ParamProviders; +use PhpBench\Attributes\Revs; class IsSingleBitBench { @@ -20,31 +20,28 @@ public function maskProvider(): Generator yield [1 << 32]; } - /** - * @Revs(100000) - * @Iterations(5) - * @ParamProviders({"maskProvider"}) - */ + /** @param int[] $mask */ + #[Revs(100000)] + #[Iterations(5)] + #[ParamProviders('maskProvider')] public function benchIsSingleBit1(array $mask): void { $this->isSingleBit1($mask[0]); } - /** - * @Revs(100000) - * @Iterations(5) - * @ParamProviders({"maskProvider"}) - */ + /** @param int[] $mask */ + #[Revs(100000)] + #[Iterations(5)] + #[ParamProviders('maskProvider')] public function benchIsSingleBit2(array $mask): void { $this->isSingleBit2($mask[0]); } - /** - * @Revs(100000) - * @Iterations(5) - * @ParamProviders({"maskProvider"}) - */ + /** @param int[] $mask */ + #[Revs(100000)] + #[Iterations(5)] + #[ParamProviders('maskProvider')] public function benchIsSingleBit3(array $mask): void { $this->isSingleBit3($mask[0]); @@ -62,10 +59,6 @@ private function isSingleBit2(int $mask): bool private function isSingleBit3(int $mask): bool { - $shift = Bits::getMostSignificantBit($mask) - 1; - if ($shift < 0) { - return false; - } - return 1 << $shift === $mask; + return 1 << Bits::getMostSignificantBit($mask) === $mask; } } diff --git a/benchmarks/UnsetBitBench.php b/benchmarks/UnsetBitBench.php index 8e3eb91..ce4a75a 100644 --- a/benchmarks/UnsetBitBench.php +++ b/benchmarks/UnsetBitBench.php @@ -5,14 +5,12 @@ namespace Yaroslavche\Benchmarks; use Generator; -use PhpBench\Benchmark\Metadata\Annotations\BeforeMethods; -use PhpBench\Benchmark\Metadata\Annotations\Iterations; -use PhpBench\Benchmark\Metadata\Annotations\ParamProviders; -use PhpBench\Benchmark\Metadata\Annotations\Revs; - -/** - * @BeforeMethods({"init"}) - */ +use PhpBench\Attributes\BeforeMethods; +use PhpBench\Attributes\Iterations; +use PhpBench\Attributes\ParamProviders; +use PhpBench\Attributes\Revs; + +#[BeforeMethods('init')] class UnsetBitBench { private int $storage; @@ -29,24 +27,22 @@ public function bitProvider(): Generator yield [PHP_INT_MAX]; } - /** - * @Revs(300000) - * @Iterations(1) - * @ParamProviders({"bitProvider"}) - */ + /** @param int[] $bit */ + #[Revs(300000)] + #[Iterations(1)] + #[ParamProviders('bitProvider')] public function benchUnsetBit1(array $bit): void { - $this->unsetBit1($bit[0], false); + $this->unsetBit1($bit[0]); } - /** - * @Revs(300000) - * @Iterations(1) - * @ParamProviders({"bitProvider"}) - */ + /** @param int[] $bit */ + #[Revs(300000)] + #[Iterations(1)] + #[ParamProviders('bitProvider')] public function benchUnsetBit2(array $bit): void { - $this->unsetBit2($bit[0], false); + $this->unsetBit2($bit[0]); } private function unsetBit1(int $bit): void diff --git a/composer.json b/composer.json index 37754ae..fe74404 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "yaroslavche/bitmask", - "description": "BitMask, IndexedBitMask, AssociativeBitMask", + "description": "BitMask, EnumBitMask", "license": "MIT", "keywords": [ "bit", @@ -8,11 +8,10 @@ "binary", "bitwise", "php", - "php7", "php8" ], "require": { - "php": "^7.4|^8.0" + "php": "^8.1" }, "require-dev": { "phpunit/phpunit": "*", @@ -50,7 +49,7 @@ "@phpcs", "@phpstan", "@phpunit", "@infection", "@coverage" ] }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true, "config": { "allow-plugins": { diff --git a/phpstan_below_81.neon b/phpstan_below_81.neon deleted file mode 100644 index 263ae93..0000000 --- a/phpstan_below_81.neon +++ /dev/null @@ -1,6 +0,0 @@ -parameters: - level: 9 - excludePaths: - analyseAndScan: - - src/EnumBitMask.php - - tests/EnumBitMaskTest.php diff --git a/src/AssociativeBitMask.php b/src/AssociativeBitMask.php deleted file mode 100644 index bcaf506..0000000 --- a/src/AssociativeBitMask.php +++ /dev/null @@ -1,105 +0,0 @@ - $keys */ - protected array $keys; - - /** - * @param array|null $keys - * @throws KeysMustBeSetException - * @throws KeysSizeMustBeEqualBitsCountException - * @todo Must be valid PHP identifier name ^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$ - * @todo research and check keys. https://www.php.net/manual/en/language.variables.basics.php - */ - public function __construct(?int $mask = null, ?int $bitsCount = null, ?array $keys = []) - { - if (empty($keys)) { - throw new KeysMustBeSetException('Third argument "$keys" must be non empty array'); - } - if ($bitsCount !== count($keys)) { - throw new KeysSizeMustBeEqualBitsCountException('Second argument "$bitsCount" must be equal to $keys array size'); - } - $this->keys = $keys; - parent::__construct($mask); - } - - /** @throws UnknownKeyException */ - final public function getByKey(string $key): bool - { - $index = array_search($key, $this->keys); - if ($index === false) { - throw new UnknownKeyException($key); - } - return $this->getByIndex(intval($index)); - } - - /** - * @param array $args - * @throws UnknownKeyException - * @throws MagicCallException - */ - final public function __call(string $method, array $args): bool - { - if (!method_exists($this, $method) && strpos($method, 'is') === 0) { - $key = lcfirst(substr($method, 2)); - return $this->getByKey($key); - } - throw new MagicCallException('Magic call should be related only for keys'); - } - - /** @throws UnknownKeyException */ - final public function __get(string $key): bool - { - return $this->getByKey($key); - } - - /** - * @throws UnknownKeyException - * @throws InvalidIndexException - * @throws NotSingleBitException - */ - final public function __set(string $key, bool $isSet): void - { - $state = $this->getByKey($key); - if ($state === $isSet) { - return; - } - /** @var int $index */ - $index = array_search($key, $this->keys); - $bit = Bits::indexToBit($index); - if ($isSet) { - $this->setBit($bit); - } else { - $this->unsetBit($bit); - } - } - - /** @throws UnknownKeyException */ - final public function __isset(string $key): bool - { - return $this->getByKey($key); - } - - /** @return bool[] */ - final public function jsonSerialize(): array - { - $array = []; - foreach ($this->keys as $index => $key) { - $array[$key] = $this->getByIndex($index); - } - return $array; - } -} diff --git a/src/BitMask.php b/src/BitMask.php index 380f835..d78435c 100644 --- a/src/BitMask.php +++ b/src/BitMask.php @@ -1,4 +1,5 @@ bitsCount = $bitsCount; - } - if (!is_null($mask)) { - $this->set($mask); - } + public function __construct( + protected int $mask = 0, + private readonly ?int $mostSignificantBit = null, + ) { + $this->set($mask); } public function __toString(): string { - return (string)$this->storage; - } - - public function __invoke(int $mask): bool - { - return $this->isSet($mask); - } - - public static function init(?int $mask = null): BitMaskInterface - { - return new static($mask); + return (string)$this->mask; } /** @inheritDoc */ - public function get(): ?int + public function get(): int { - return $this->storage; + return $this->mask; } /** @inheritDoc */ public function set(int $mask): void { - if ($mask < 0 || (!is_null($this->bitsCount) && $mask >= $this->bitsCount ** 2)) { - throw new OutOfRangeException((string)$mask); - } - $this->storage = $mask; + $this->checkMask($mask); + $this->mask = $mask; } /** @inheritDoc */ public function unset(): void { - $this->storage = null; + $this->mask = 0; } /** @inheritDoc */ public function isSet(int $mask): bool { - return ($this->storage & $mask) === $mask; + return ($this->mask & $mask) === $mask; } - /** - * @throws NotSingleBitException - * @throws OutOfRangeException - */ - private function checkBit(int $bit): void + /** @inheritDoc */ + public function setBits(int ...$bits): void { - if (!Bits::isSingleBit($bit)) { - throw new NotSingleBitException((string)$bit); - } - if (!is_null($this->bitsCount) && $bit >= $this->bitsCount ** 2) { - throw new OutOfRangeException((string)$bit); - } + array_walk($bits, fn(int $bit) => $this->checkBit($bit)); + array_walk($bits, fn(int $bit) => $this->mask |= $bit); } /** @inheritDoc */ - public function setBit(int $bit): void + public function unsetBits(int ...$bits): void { - $this->checkBit($bit); - $this->storage |= $bit; + array_walk($bits, fn(int $bit) => $this->checkBit($bit)); + array_walk($bits, fn(int $bit) => $this->mask ^= $bit); + // $this->mask &= ~$bit; } /** @inheritDoc */ - public function unsetBit(int $bit): void + public function isSetBits(int ...$bits): bool { - $this->checkBit($bit); - $this->storage ^= $bit; -// $this->storage &= ~$bit; + array_walk($bits, fn(int $bit) => $this->checkBit($bit)); + return !in_array(false, array_map(fn(int $bit) => $this->isSet($bit), $bits), true); } /** @inheritDoc */ - public function isSetBit(int $bit): bool + public function setBitByShiftOffset(int $shiftOffset): void { - $this->checkBit($bit); - return $this->isSet($bit); + $this->setBits(1 << $shiftOffset); } - private function checkShiftOffset(int $shiftOffset): void + /** @inheritDoc */ + public function unsetBitByShiftOffset(int $shiftOffset): void { - if ($shiftOffset < 0 || (null !== $this->bitsCount && $this->bitsCount <= $shiftOffset)) { - throw new OutOfRangeException((string)$shiftOffset); - } + $this->unsetBits(1 << $shiftOffset); } /** @inheritDoc */ - public function setBitByShiftOffset(int $shiftOffset): void + public function isSetBitByShiftOffset(int $shiftOffset): bool { - $this->checkShiftOffset($shiftOffset); - $bit = 1 << $shiftOffset; - $this->setBit($bit); + return $this->isSetBits(1 << $shiftOffset); } - /** @inheritDoc */ - public function unsetBitByShiftOffset(int $shiftOffset): void + /** @throws OutOfRangeException */ + private function checkMask(int $mask): void { - $this->checkShiftOffset($shiftOffset); - $bit = 1 << $shiftOffset; - $this->unsetBit($bit); + if ($mask < 0 || $this->mostSignificantBit && $mask >= Bits::indexToBit($this->mostSignificantBit + 1)) { + throw new OutOfRangeException((string)$mask); + } } - /** @inheritDoc */ - public function isSetBitByShiftOffset(int $shiftOffset): bool + /** + * @throws NotSingleBitException + * @throws OutOfRangeException + */ + private function checkBit(int $bit): void { - $this->checkShiftOffset($shiftOffset); - $bit = 1 << $shiftOffset; - return $this->isSetBit($bit); + $this->checkMask($bit); + if (!Bits::isSingleBit($bit)) { + throw new NotSingleBitException((string)$bit); + } } } diff --git a/src/BitMaskInterface.php b/src/BitMaskInterface.php index b40870a..ce2ce4d 100644 --- a/src/BitMaskInterface.php +++ b/src/BitMaskInterface.php @@ -1,4 +1,5 @@ get(); => (int) 7 - * - * @return int|null - * @noinspection PhpMethodNamingConventionInspection */ - public function get(): ?int; + public function get(): int; /** - * Set BitMask integer value. - * * $mask = new BitMask(); * $mask->set(5); * $mask->get(); => (int) 5 - * - * @param int $mask * @noinspection PhpMethodNamingConventionInspection */ public function set(int $mask): void; /** - * Unset BitMask value. - * * $mask = new BitMask(5); * $mask->unset(); - * $mask->get(); => null + * $mask->get(); => 0 */ public function unset(): void; /** - * Check if $mask is set in BitMask integer value. - * * $mask = new BitMask(1 << 1); * $mask->isSet(1); => false * $mask->isSet(2); => true - * - * @param int $mask - * @return bool */ public function isSet(int $mask): bool; /** - * Set $bit in BitMask integer value. - * - * $mask = new BitMask(0); - * $mask->setBit(1); - * $mask->setBit(4); + * $mask = new BitMask(); + * $mask->setBits(1, 4); * $mask->get(); => 5 * $mask->setBit(3) => NotSingleBitException * - * @param int $bit * @throws NotSingleBitException */ - public function setBit(int $bit): void; + public function setBits(int ...$bits): void; /** - * Unset $bit in BitMask integer value. - * * $mask = new BitMask(7); - * $mask->unsetBit(2); + * $mask->unsetBits(2); * $mask->get(); => 5 * $mask->unsetBit(5) => NotSingleBitException * - * @param int $bit * @throws NotSingleBitException */ - public function unsetBit(int $bit): void; + public function unsetBits(int ...$bits): void; /** - * Check if $bit is set in BitMask integer value. - * * $mask = new BitMask(5); - * $mask->isSetBit(1); => true - * $mask->isSetBit(2); => false - * $mask->isSetBit(4); => true - * $mask->unsetBit(5); => NotSingleBitException + * $mask->isSetBits(1, 4); => true + * $mask->isSetBits(2); => false + * $mask->isSetBits(5); => NotSingleBitException * - * @param int $bit - * @return bool * @throws NotSingleBitException */ - public function isSetBit(int $bit): bool; + public function isSetBits(int ...$bits): bool; /** - * Set bit in shift offset of BitMask integer value. - * * $mask = new BitMask(); * $mask->setBitByOffset(0); * $mask->get(); => 1 @@ -114,17 +75,13 @@ public function isSetBit(int $bit): bool; * $mask->get(); => 3 * $mask->setBitByOffset(2); * $mask->get(); => 7 - * $mask->setBitByOffset(-1) => OutOfRangeException * - * * Or possible map in inverse direction with $inverseMask << abs($shiftOffset), but seems weird and not needed + * $mask->setBitByOffset(-1) => OutOfRangeException * - * @param int $shiftOffset * @throws OutOfRangeException */ public function setBitByShiftOffset(int $shiftOffset): void; /** - * Unset bit in shift offset of BitMask integer value. - * * $mask = new BitMask(5); * $mask->unsetBitByOffset(0); * $mask->get(); => 4 @@ -132,24 +89,19 @@ public function setBitByShiftOffset(int $shiftOffset): void; * $mask->get(); => 4 * $mask->unsetBitByOffset(2); * $mask->get(); => 0 - * $mask->unsetBitByOffset(-1) => OutOfRangeException * + * $mask->unsetBitByOffset(-1) => OutOfRangeException * - * @param int $shiftOffset * @throws OutOfRangeException */ public function unsetBitByShiftOffset(int $shiftOffset): void; /** - * Check if $bit is set in shift offset of BitMask integer value. - * * $mask = new BitMask(2); * $mask->isSetBitByOffset(0); => false * $mask->isSetBitByOffset(1); => true * $mask->isSetBitByOffset(2); => false - * $mask->isSetBitByOffset(-1) => OutOfRangeException * + * $mask->isSetBitByOffset(-1) => OutOfRangeException * - * @param int $shiftOffset - * @return bool * @throws OutOfRangeException */ public function isSetBitByShiftOffset(int $shiftOffset): bool; diff --git a/src/EnumBitMask.php b/src/EnumBitMask.php index 2a2a6f7..813da1b 100644 --- a/src/EnumBitMask.php +++ b/src/EnumBitMask.php @@ -1,79 +1,63 @@ -= 80100 || throw new UnsupportedPhpVersionException('Requires PHP 8.1 interface UnitEnum'); -class EnumBitMask +final class EnumBitMask extends BitMask implements BitMaskInterface { - private int $bitmask = 0; - /** @var UnitEnum[] $keys */ - private array $keys = []; + /** @var array $map case => bit */ + private array $map = []; /** - * @param class-string $maskEnum + * @param class-string $enum * @throws UnknownEnumException */ public function __construct( - private readonly string $maskEnum, - UnitEnum ...$bits, + private readonly string $enum, + protected int $mask = 0, ) { - if (!is_subclass_of($this->maskEnum, UnitEnum::class)) { - throw new UnknownEnumException('BitMask enum must be instance of UnitEnum'); + if (!is_subclass_of($this->enum, UnitEnum::class)) { + throw new UnknownEnumException('EnumBitMask enum must be subclass of UnitEnum'); } - $this->keys = $this->maskEnum::cases(); - $this->set(...$bits); - } - - public function get(): int - { - return $this->bitmask; + foreach ($this->enum::cases() as $index => $case) { + $this->map[strval($case->name)] = Bits::indexToBit($index); + } + parent::__construct($this->mask, count($this->enum::cases()) - 1); } /** @throws UnknownEnumException */ - public function set(UnitEnum ...$bits): void + public function setEnumBits(UnitEnum ...$bits): void { - foreach ($bits as $bit) { - if (!$this->isSet($bit)) { - $this->bitmask += 1 << intval(array_search($bit, $this->keys)); - } - } + $this->isSetEnumBits(...$bits); + $this->setBits(...$this->enumBitsToBits(...$bits)); } /** @throws UnknownEnumException */ - public function unset(UnitEnum ...$bits): void + public function unsetEnumBits(UnitEnum ...$bits): void { - foreach ($bits as $bit) { - if ($this->isSet($bit)) { - $this->bitmask -= 1 << intval(array_search($bit, $this->keys)); - } - } + $this->isSetEnumBits(...$bits); + $this->unsetBits(...$this->enumBitsToBits(...$bits)); } /** @throws UnknownEnumException */ - public function isSet(UnitEnum ...$bits): bool + public function isSetEnumBits(UnitEnum ...$bits): bool { - foreach ($bits as $bit) { - $this->checkEnumCase($bit); - $mask = 1 << intval(array_search($bit, $this->keys)); - if (($this->bitmask & $mask) !== $mask) { - return false; - } - } - return true; + array_walk( + $bits, + fn(UnitEnum $bit) => $bit instanceof $this->enum || + throw new UnknownEnumException(sprintf('Expected %s enum, %s provided', $this->enum, $bit::class)) + ); + return $this->isSetBits(...$this->enumBitsToBits(...$bits)); } - /** @throws UnknownEnumException */ - private function checkEnumCase(UnitEnum $case): void + /** @return int[] */ + private function enumBitsToBits(UnitEnum ...$bits): array { - $case instanceof $this->maskEnum || - throw new UnknownEnumException(sprintf('Expected %s enum case, %s provided', $this->maskEnum, $case::class)); + return array_map(fn(UnitEnum $bit) => $this->map[$bit->name], $bits); } } diff --git a/src/Exception/BitMaskExceptionInterface.php b/src/Exception/BitMaskExceptionInterface.php new file mode 100644 index 0000000..5bc5fc9 --- /dev/null +++ b/src/Exception/BitMaskExceptionInterface.php @@ -0,0 +1,9 @@ +isSetBitByShiftOffset($index); - } -} diff --git a/src/Util/Bits.php b/src/Util/Bits.php index 04a0eb0..2069852 100644 --- a/src/Util/Bits.php +++ b/src/Util/Bits.php @@ -4,32 +4,18 @@ namespace BitMask\Util; -use BitMask\Exception\InvalidIndexException; use BitMask\Exception\NotSingleBitException; +use BitMask\Exception\OutOfRangeException; final class Bits { - /** - * get most significant bit position (right -> left) - * @example 10001 -> 5, 0010 -> 2, 00100010 -> 6 - * @todo research and use (if needed) https://www.geeksforgeeks.org/find-significant-set-bit-number/ + * get most significant bit position (right -> left, index) + * @example 10001 -> 4, 0010 -> 1, 00100010 -> 5 */ public static function getMostSignificantBit(int $mask): int { - $scan = 1; - $mostSignificantBit = 0; - while ($mask >= $scan) { - $mostSignificantBit++; - $scan <<= 1; - } - return $mostSignificantBit; - } - - /** @deprecated use getMostSignificantBit instead */ - public static function getMSB(int $mask): int - { - return self::getMostSignificantBit($mask); + return (int)log($mask, 2); } /** @@ -55,13 +41,10 @@ public static function getSetBits(int $mask): array * @example 1000 => true, 010100 => false, 0000100 => true * @see benchmarks/IsSingleBitBench.php * ./vendor/bin/phpbench run benchmarks/IsSingleBitBench.php --report=default - * - * @todo research maybe getMSB must return shift offset and then isSingleBit3 might be faster - * return 1 << BitUtils::getMSB($mask) === $mask; */ public static function isSingleBit(int $mask): bool { - return count(self::getSetBits($mask)) === 1; + return 1 << Bits::getMostSignificantBit($mask) === $mask; } /** @@ -73,20 +56,21 @@ public static function bitToIndex(int $mask): int if (!self::isSingleBit($mask)) { throw new NotSingleBitException('Argument must be a single bit'); } - return (int)log($mask, 2); + return self::getMostSignificantBit($mask); } /** * index to single bit * @example 0 => 0b1 (1), 1 => 0b10 (2), 2 => 0b100 (4), ... - * @throws InvalidIndexException * @see benchmarks/IndexToBitBench.php * ./vendor/bin/phpbench run benchmarks/IndexToBitBench.php --report=default + * + * @throws OutOfRangeException */ public static function indexToBit(int $index): int { if ($index < 0) { - throw new InvalidIndexException('Index (zero based) must be greater than or equal to zero'); + throw new OutOfRangeException((string)$index); } return 1 << $index; } @@ -105,7 +89,7 @@ public static function getSetBitsIndexes(int $mask): array { $bitIndexes = []; foreach (self::getSetBits($mask) as $index => $bit) { - $bitIndexes[$index] = intval(log($bit, 2)); + $bitIndexes[$index] = self::getMostSignificantBit($bit); } return $bitIndexes; } diff --git a/tests/AssociativeBitMaskTest.php b/tests/AssociativeBitMaskTest.php deleted file mode 100644 index f898f4b..0000000 --- a/tests/AssociativeBitMaskTest.php +++ /dev/null @@ -1,111 +0,0 @@ -expectException(KeysMustBeSetException::class); - $this->expectExceptionMessage('Third argument "$keys" must be non empty array'); - new AssociativeBitMask(7, 3, []); - } - - public function testBitsNotEqualKeys(): void - { - $this->expectException(KeysSizeMustBeEqualBitsCountException::class); - $this->expectExceptionMessage('Second argument "$bitsCount" must be equal to $keys array size'); - new AssociativeBitMask(7, 3, ['test']); - } - - public function testGetByKey(): void - { - $bitmask = new AssociativeBitMask(7, 3, ['readable', 'writable', 'executable']); - assertTrue($bitmask->getByKey('readable')); - assertTrue($bitmask->getByKey('writable')); - assertTrue($bitmask->getByKey('executable')); - $this->expectException(UnknownKeyException::class); - $this->expectExceptionMessage('unknown'); - $bitmask->getByKey('unknown'); - } - - public function testMagicGet(): void - { - $bitmask = new AssociativeBitMask(7, 3, ['readable', 'writable', 'executable']); - assertTrue($bitmask->readable); - assertTrue($bitmask->writable); - assertTrue($bitmask->executable); - $this->expectException(UnknownKeyException::class); - $this->expectExceptionMessage('unknown'); - $bitmask->unknown; - } - - - public function testMagicCall(): void - { - $bitmask = new AssociativeBitMask(7, 3, ['readable', 'writable', 'executable']); - assertTrue($bitmask->isReadable()); - assertTrue($bitmask->isWritable()); - assertTrue($bitmask->isExecutable()); - $this->expectException(MagicCallException::class); - $this->expectExceptionMessage('Magic call should be related only for keys'); - $bitmask->invalidMethodName(); - } - - public function testMagicIsSet(): void - { - $bitmask = new AssociativeBitMask(7, 3, ['readable', 'writable', 'executable']); - assertTrue(isset($bitmask->readable)); - assertTrue(isset($bitmask->writable)); - assertTrue(isset($bitmask->executable)); - } - - - public function testMagicSet(): void - { - $bitmask = new AssociativeBitMask(5, 3, ['readable', 'writable', 'executable']); - $bitmask->readable = false; - assertFalse($bitmask->readable); - $bitmask->writable = true; - assertTrue($bitmask->writable); - $bitmask->executable = false; - assertFalse($bitmask->executable); - $bitmask->executable = false; - try { - $bitmask->unknownKey = true; - } catch (UnknownKeyException $exception) { - assertSame('unknownKey', $exception->getMessage()); - } - } - - public function testJsonSerialize(): void - { - $bitmask = new AssociativeBitMask(7, 3, ['readable', 'writable', 'executable']); - assertSame(['readable' => true, 'writable' => true, 'executable' => true], $bitmask->jsonSerialize()); - } - - public function testIssue18(): void - { - $bitmask = new AssociativeBitMask(7, 3, ['readable', 'writable', 'executable']); - assertTrue($bitmask->isReadable()); - assertTrue($bitmask->isWritable()); - assertTrue($bitmask->isExecutable()); - $bitmask->set(1); - assertTrue($bitmask->isReadable()); - assertFalse($bitmask->isWritable()); - assertFalse($bitmask->isExecutable()); - } -} diff --git a/tests/BitMaskTest.php b/tests/BitMaskTest.php index b49ad2d..d9bb71e 100644 --- a/tests/BitMaskTest.php +++ b/tests/BitMaskTest.php @@ -12,7 +12,6 @@ use function PHPUnit\Framework\assertEquals; use function PHPUnit\Framework\assertFalse; use function PHPUnit\Framework\assertInstanceOf; -use function PHPUnit\Framework\assertNull; use function PHPUnit\Framework\assertSame; use function PHPUnit\Framework\assertTrue; @@ -22,125 +21,135 @@ class BitMaskTest extends TestCase private const WRITE = 1 << 1; private const EXECUTE = 1 << 2; - public function testBitMask() + public function testBitMask(): void { $bitmask = new BitMask(); assertInstanceOf(BitMask::class, $bitmask); - assertNull($bitmask->get()); + assertSame(0, $bitmask->get()); + $this->expectException(OutOfRangeException::class); + new BitMask(-2); } - public function testSet() + public function testBitMaskConstructOutOfRange(): void { - $bitmask = new BitMask(null, 2); - $bitmask->set(static::READ); - assertEquals(static::READ, $bitmask->get()); - $bitmask->set(0); // check mutation LessThanOrEqualTo BitMask.php:68 + $bitmask = new BitMask(15, 3); + assertInstanceOf(BitMask::class, $bitmask); + assertSame(15, $bitmask->get()); $this->expectException(OutOfRangeException::class); - $this->expectExceptionMessage((string)static::EXECUTE); - $bitmask->set(static::EXECUTE); + new BitMask(16, 3); } - public function testUnset() + public function testSet(): void { - $bitmask = new BitMask(static::READ | static::EXECUTE); + $bitmask = new BitMask(0, 1); + $bitmask->set(self::READ); + assertEquals(self::READ, $bitmask->get()); + $this->expectException(OutOfRangeException::class); + $this->expectExceptionMessage((string)self::EXECUTE); + $bitmask->set(self::EXECUTE); + } + + public function testUnset(): void + { + $bitmask = new BitMask(self::READ | self::EXECUTE); $bitmask->unset(); - assertNull($bitmask->get()); + assertSame(0, $bitmask->get()); } - public function testIsSet() + public function testIsSet(): void { - $bitmask = new BitMask(static::WRITE | static::EXECUTE); - assertTrue($bitmask->isSet(static::WRITE | static::EXECUTE)); - assertFalse($bitmask->isSet(static::READ)); - assertTrue($bitmask->isSet(static::WRITE)); - assertTrue($bitmask->isSet(static::EXECUTE)); - assertFalse($bitmask->isSet(static::READ | static::WRITE)); - assertFalse($bitmask->isSet(static::READ | static::EXECUTE)); - assertFalse($bitmask->isSet(static::READ | static::WRITE | static::EXECUTE)); + $bitmask = new BitMask(self::WRITE | self::EXECUTE); + assertTrue($bitmask->isSet(self::WRITE | self::EXECUTE)); + assertFalse($bitmask->isSet(self::READ)); + assertTrue($bitmask->isSet(self::WRITE)); + assertTrue($bitmask->isSet(self::EXECUTE)); + assertFalse($bitmask->isSet(self::READ | self::WRITE)); + assertFalse($bitmask->isSet(self::READ | self::EXECUTE)); + assertFalse($bitmask->isSet(self::READ | self::WRITE | self::EXECUTE)); } - public function testIsSetBit() + public function testIsSetBit(): void { - $bitmask = new BitMask(static::READ | static::WRITE | static::EXECUTE, 3); - assertTrue($bitmask->isSetBit(static::READ)); - assertTrue($bitmask->isSetBit(static::WRITE)); - assertTrue($bitmask->isSetBit(static::EXECUTE)); + $bitmask = new BitMask(self::READ | self::WRITE | self::EXECUTE, 3); + assertTrue($bitmask->isSetBits(self::READ)); + assertTrue($bitmask->isSetBits(self::WRITE)); + assertTrue($bitmask->isSetBits(self::EXECUTE)); $bitmask->unset(); - assertFalse($bitmask->isSetBit(static::READ)); - assertFalse($bitmask->isSetBit(static::WRITE)); - assertFalse($bitmask->isSetBit(static::EXECUTE)); + assertFalse($bitmask->isSetBits(self::READ)); + assertFalse($bitmask->isSetBits(self::WRITE)); + assertFalse($bitmask->isSetBits(self::EXECUTE)); } - public function testIsSetBitNotSingleBit() + public function testIsSetBitNotSingleBit(): void { $bitmask = new BitMask(); $this->expectException(NotSingleBitException::class); - $this->expectExceptionMessage((string)(static::READ | static::WRITE)); - $bitmask->isSetBit(static::READ | static::WRITE); + $this->expectExceptionMessage((string)(self::READ | self::WRITE)); + $bitmask->isSetBits(self::READ | self::WRITE); } - public function testIsSetBitOutOfRange() + public function testIsSetBitOutOfRange(): void { - $bitmask = new BitMask(null, 2); + $bitmask = new BitMask(0, 1); $this->expectException(OutOfRangeException::class); - $this->expectExceptionMessage((string)static::EXECUTE); - $bitmask->isSetBit(static::EXECUTE); + $this->expectExceptionMessage((string)self::EXECUTE); + $bitmask->isSetBits(self::EXECUTE); } - public function testSetBit() + public function testSetBit(): void { $bitmask = new BitMask(); - $bitmask->setBit(static::READ); - assertTrue($bitmask->isSetBit(static::READ)); - assertSame(static::READ, $bitmask->get()); - $bitmask->setBit(static::WRITE); - assertSame(static::READ | static::WRITE, $bitmask->get()); + $bitmask->setBits(self::READ); + assertTrue($bitmask->isSetBits(self::READ)); + assertSame(self::READ, $bitmask->get()); + $bitmask->setBits(self::WRITE); + assertSame(self::READ | self::WRITE, $bitmask->get()); } - public function testSetBitNotSingleBit() + public function testSetBitNotSingleBit(): void { $bitmask = new BitMask(); $this->expectException(NotSingleBitException::class); - $this->expectExceptionMessage((string)(static::READ | static::WRITE)); - $bitmask->setBit(static::READ | static::WRITE); + $this->expectExceptionMessage((string)(self::READ | self::WRITE)); + $bitmask->setBits(self::READ | self::WRITE); } - public function testSetBitOutOfRange() + public function testSetBitOutOfRange(): void { - $bitmask = new BitMask(null, 2); + $bitmask = new BitMask(0, 1); $this->expectException(OutOfRangeException::class); - $this->expectExceptionMessage((string)static::EXECUTE); - $bitmask->setBit(static::EXECUTE); + $this->expectExceptionMessage((string)self::EXECUTE); + $bitmask->setBits(self::EXECUTE); } - public function testUnsetBit() + public function testUnsetBit(): void { - $bitmask = new BitMask(static::READ | static::WRITE); - $bitmask->unsetBit(static::READ); - assertFalse($bitmask->isSetBit(static::READ)); - assertTrue($bitmask->isSetBit(static::WRITE)); - $bitmask->unsetBit(static::WRITE); - assertFalse($bitmask->isSetBit(static::READ)); - assertFalse($bitmask->isSetBit(static::WRITE)); + $bitmask = new BitMask(self::READ | self::WRITE); + $bitmask->unsetBits(self::READ); + assertFalse($bitmask->isSetBits(self::READ)); + assertTrue($bitmask->isSetBits(self::WRITE)); + $bitmask->unsetBits(self::WRITE); + assertFalse($bitmask->isSetBits(self::READ)); + assertFalse($bitmask->isSetBits(self::WRITE)); } - public function testUnsetBitNotSingleBit() + public function testUnsetBitNotSingleBit(): void { - $bitmask = new BitMask(static::EXECUTE); + $bitmask = new BitMask(self::EXECUTE); $this->expectException(NotSingleBitException::class); - $this->expectExceptionMessage((string)(static::READ | static::WRITE)); - $bitmask->unsetBit(static::READ | static::WRITE); + $this->expectExceptionMessage((string)(self::READ | self::WRITE)); + $bitmask->unsetBits(self::READ | self::WRITE); } - public function testUnsetBitOutOfRange() + public function testUnsetBitOutOfRange(): void { - $bitmask = new BitMask(static::WRITE, 2); + $bitmask = new BitMask(self::WRITE, 1); $this->expectException(OutOfRangeException::class); - $this->expectExceptionMessage((string)static::EXECUTE); - $bitmask->unsetBit(static::EXECUTE); + $this->expectExceptionMessage((string)self::EXECUTE); + $bitmask->unsetBits(self::EXECUTE); } - public function testToString() + public function testToString(): void { $bitmask = new BitMask(7); assertSame('7', (string)$bitmask); @@ -148,71 +157,56 @@ public function testToString() assertSame('9', (string)$bitmask); } - public function testInvoke() - { - $bitmask = new BitMask(7); - assertTrue($bitmask(1)); - assertFalse($bitmask(8)); - $bitmask->set(9); - assertTrue($bitmask(8)); - assertFalse($bitmask(4)); - } - - public function testInit() - { - $bitmask = BitMask::init(7); - assertInstanceOf(BitMask::class, $bitmask); - } - - public function testSetBitByShiftOffset() + public function testSetBitByShiftOffset(): void { - $bitmask = new BitMask(null, 3); + $bitmask = new BitMask(0, 3); $bitmask->setBitByShiftOffset(0); - assertTrue($bitmask->isSetBit(static::READ)); + assertTrue($bitmask->isSetBits(self::READ)); $bitmask->setBitByShiftOffset(1); - assertTrue($bitmask->isSetBit(static::WRITE)); + assertTrue($bitmask->isSetBits(self::WRITE)); $bitmask->setBitByShiftOffset(2); - assertTrue($bitmask->isSetBit(static::EXECUTE)); - assertSame(static::READ | static::WRITE | static::EXECUTE, $bitmask->get()); + assertTrue($bitmask->isSetBits(self::EXECUTE)); + assertSame(self::READ | self::WRITE | self::EXECUTE, $bitmask->get()); } - public function testUnsetBitByShiftOffset() + public function testUnsetBitByShiftOffset(): void { - $bitmask = new BitMask(static::READ | static::WRITE | static::EXECUTE); + $bitmask = new BitMask(self::READ | self::WRITE | self::EXECUTE); $bitmask->unsetBitByShiftOffset(0); - assertFalse($bitmask->isSetBit(static::READ)); + assertFalse($bitmask->isSetBits(self::READ)); $bitmask->unsetBitByShiftOffset(1); - assertFalse($bitmask->isSetBit(static::WRITE)); + assertFalse($bitmask->isSetBits(self::WRITE)); $bitmask->unsetBitByShiftOffset(2); - assertFalse($bitmask->isSetBit(static::EXECUTE)); + assertFalse($bitmask->isSetBits(self::EXECUTE)); assertSame(0, $bitmask->get()); } - public function testIsSetBitByShiftOffset() + public function testIsSetBitByShiftOffset(): void { - $bitmask = new BitMask(static::READ | static::WRITE | static::EXECUTE); + $bitmask = new BitMask(self::READ | self::WRITE | self::EXECUTE); assertTrue($bitmask->isSetBitByShiftOffset(0)); assertTrue($bitmask->isSetBitByShiftOffset(1)); assertTrue($bitmask->isSetBitByShiftOffset(2)); } - public function testShiftOffsetOutOfRange() + public function testShiftOffsetOutOfRange(): void { - $bitmask = new BitMask(static::WRITE, 2); + assertSame(true, true); + $bitmask = new BitMask(self::READ, 2); try { - $bitmask->setBitByShiftOffset(2); + $bitmask->setBitByShiftOffset(3); } catch (OutOfRangeException $exception) { - assertSame('2', $exception->getMessage()); + assertSame('8', $exception->getMessage()); } try { - $bitmask->unsetBitByShiftOffset(2); + $bitmask->unsetBitByShiftOffset(3); } catch (OutOfRangeException $exception) { - assertSame('2', $exception->getMessage()); + assertSame('8', $exception->getMessage()); } try { - $bitmask->isSetBitByShiftOffset(2); + $bitmask->isSetBitByShiftOffset(3); } catch (OutOfRangeException $exception) { - assertSame('2', $exception->getMessage()); + assertSame('8', $exception->getMessage()); } } } diff --git a/tests/EnumBitMaskTest.php b/tests/EnumBitMaskTest.php index 73f6964..c34b758 100644 --- a/tests/EnumBitMaskTest.php +++ b/tests/EnumBitMaskTest.php @@ -5,24 +5,19 @@ namespace BitMask\Tests; use BitMask\EnumBitMask; +use BitMask\Exception\OutOfRangeException; use BitMask\Exception\UnknownEnumException; +use BitMask\Tests\fixtures\Enum\BackedInt; +use BitMask\Tests\fixtures\Enum\BackedString; use BitMask\Tests\fixtures\Enum\Permissions; use BitMask\Tests\fixtures\Enum\Unknown; use PHPUnit\Framework\TestCase; use function PHPUnit\Framework\assertFalse; use function PHPUnit\Framework\assertSame; use function PHPUnit\Framework\assertTrue; -use const PHP_VERSION_ID; final class EnumBitMaskTest extends TestCase { - protected function setUp(): void - { - if (PHP_VERSION_ID < 80100) { - $this->markTestSkipped('PHP ^8.1 only'); - } - } - public function testNotAnEnum(): void { $this->expectException(UnknownEnumException::class); @@ -31,43 +26,77 @@ public function testNotAnEnum(): void public function testUnknownEnum(): void { + $bitmask = new EnumBitMask(Permissions::class); $this->expectException(UnknownEnumException::class); - new EnumBitMask(Permissions::class, Unknown::Case); + $bitmask->setEnumBits(Unknown::Case); + } + + public function testConstructWithDefaultMask(): void + { + $enumBitmask = new EnumBitMask(Permissions::class); + assertSame(0, $enumBitmask->get()); } public function testGet(): void { - $enumBitmask = new EnumBitMask(Permissions::class, Permissions::Create, Permissions::Read); - assertSame(3, $enumBitmask->get()); + $enumBitmask = new EnumBitMask(Permissions::class, 8); + assertSame(8, $enumBitmask->get()); $this->expectException(UnknownEnumException::class); - $enumBitmask->isSet(Unknown::Case); + $enumBitmask->isSetEnumBits(Unknown::Case); + } + + public function testSetOutOfRange(): void + { + new EnumBitMask(Permissions::class, 15); + $this->expectException(OutOfRangeException::class); + new EnumBitMask(Permissions::class, 16); } public function testIsSet(): void { - $enumBitmask = new EnumBitMask(Permissions::class, Permissions::Create, Permissions::Read); - assertTrue($enumBitmask->isSet(Permissions::Create)); - assertTrue($enumBitmask->isSet(Permissions::Read)); - assertFalse($enumBitmask->isSet(Permissions::Update)); - assertFalse($enumBitmask->isSet(Permissions::Delete)); + $enumBitmask = new EnumBitMask(Permissions::class, 3); + assertTrue($enumBitmask->isSetEnumBits(Permissions::Create)); + assertTrue($enumBitmask->isSetEnumBits(Permissions::Read)); + assertFalse($enumBitmask->isSetEnumBits(Permissions::Update)); + assertFalse($enumBitmask->isSetEnumBits(Permissions::Delete)); $this->expectException(UnknownEnumException::class); - $enumBitmask->isSet(Unknown::Case); + $enumBitmask->isSetEnumBits(Unknown::Case); } public function testSetUnset(): void { - $enumBitmask = new EnumBitMask(Permissions::class, Permissions::Create, Permissions::Read); - $enumBitmask->unset(Permissions::Create, Permissions::Read); - assertFalse($enumBitmask->isSet(Permissions::Create)); - assertFalse($enumBitmask->isSet(Permissions::Read)); - assertFalse($enumBitmask->isSet(Permissions::Read, Permissions::Update)); + $enumBitmask = new EnumBitMask(Permissions::class, 3); + $enumBitmask->unsetEnumBits(Permissions::Create, Permissions::Read); + assertFalse($enumBitmask->isSetEnumBits(Permissions::Create)); + assertFalse($enumBitmask->isSetEnumBits(Permissions::Read)); + assertFalse($enumBitmask->isSetEnumBits(Permissions::Read, Permissions::Update)); assertSame(0, $enumBitmask->get()); - $enumBitmask->set(Permissions::Update, Permissions::Delete); - assertTrue($enumBitmask->isSet(Permissions::Update)); - assertTrue($enumBitmask->isSet(Permissions::Delete)); - assertTrue($enumBitmask->isSet(Permissions::Update, Permissions::Delete)); + $enumBitmask->setEnumBits(Permissions::Update, Permissions::Delete); + assertTrue($enumBitmask->isSetEnumBits(Permissions::Update)); + assertTrue($enumBitmask->isSetEnumBits(Permissions::Delete)); + assertTrue($enumBitmask->isSetEnumBits(Permissions::Update, Permissions::Delete)); assertSame(12, $enumBitmask->get()); $this->expectException(UnknownEnumException::class); - $enumBitmask->unset(Unknown::Case); + $enumBitmask->unsetEnumBits(Unknown::Case); + } + + public function testBackedEnum(): void + { + // backed string + $backedStringEnumBitmask = new EnumBitMask(BackedString::class, 3); + assertTrue($backedStringEnumBitmask->isSetEnumBits(BackedString::Create, BackedString::Read)); + assertFalse($backedStringEnumBitmask->isSetEnumBits(BackedString::Update, BackedString::Delete)); + $backedStringEnumBitmask->unsetEnumBits(BackedString::Create, BackedString::Read); + $backedStringEnumBitmask->setEnumBits(BackedString::Update, BackedString::Delete); + assertFalse($backedStringEnumBitmask->isSetEnumBits(BackedString::Create, BackedString::Read)); + assertTrue($backedStringEnumBitmask->isSetEnumBits(BackedString::Update, BackedString::Delete)); + // backed int + $backedIntEnumBitmask = new EnumBitMask(BackedInt::class, 3); + assertTrue($backedIntEnumBitmask->isSetEnumBits(BackedInt::Create, BackedInt::Read)); + assertFalse($backedIntEnumBitmask->isSetEnumBits(BackedInt::Update, BackedInt::Delete)); + $backedIntEnumBitmask->unsetEnumBits(BackedInt::Create, BackedInt::Read); + $backedIntEnumBitmask->setEnumBits(BackedInt::Update, BackedInt::Delete); + assertFalse($backedIntEnumBitmask->isSetEnumBits(BackedInt::Create, BackedInt::Read)); + assertTrue($backedIntEnumBitmask->isSetEnumBits(BackedInt::Update, BackedInt::Delete)); } } diff --git a/tests/IndexedBitMaskTest.php b/tests/IndexedBitMaskTest.php deleted file mode 100644 index 9c84c49..0000000 --- a/tests/IndexedBitMaskTest.php +++ /dev/null @@ -1,101 +0,0 @@ -get()); - } - - public function testGetByIndex() - { - $bitmask = new IndexedBitMask(5, 3); - assertTrue($bitmask->getByIndex(0)); - assertFalse($bitmask->getByIndex(1)); - assertTrue($bitmask->getByIndex(2)); - try { - $bitmask->getByIndex(-1); - } catch (OutOfRangeException $exception) { - assertSame('-1', $exception->getMessage()); - } - try { - $bitmask->getByIndex(3); - } catch (OutOfRangeException $exception) { - assertSame('3', $exception->getMessage()); - } - } - - public function testSet() - { - $bitmask = new IndexedBitMask(); - $bitmask->set(7); - assertEquals(7, $bitmask->get()); - $bitmask->set(0); - assertEquals(0, $bitmask->get()); - } - - public function testUnset() - { - $bitmask = new IndexedBitMask(7); - $bitmask->unset(); - assertEquals(0, $bitmask->get()); - } - - public function testIsSet() - { - $bitmask = new IndexedBitMask(7); - assertTrue($bitmask->isSet(7)); - $bitmask->set(0); - assertFalse($bitmask->isSet(7)); - } - - public function testIsSetBit() - { - $bitmask = new IndexedBitMask(7); - assertFalse($bitmask->isSetBit(8)); - assertTrue($bitmask->isSetBit(4)); - } - - public function testSetBit() - { - $bitmask = new IndexedBitMask(); - $bitmask->setBit(8); - assertTrue($bitmask->isSetBit(8)); - $this->expectExceptionObject(new NotSingleBitException('3')); - $bitmask->setBit(3); - assertEquals(8, $bitmask->get()); - } - - public function testUnsetBit() - { - $bitmask = new IndexedBitMask(); - $bitmask->setBit(8); - $bitmask->unsetBit(8); - assertFalse($bitmask->isSetBit(8)); - $this->expectExceptionObject(new NotSingleBitException('3')); - $bitmask->unsetBit(3); - assertEquals(0, $bitmask->get()); - } -} diff --git a/tests/Util/BitsTest.php b/tests/Util/BitsTest.php index 5d0155b..63465c4 100644 --- a/tests/Util/BitsTest.php +++ b/tests/Util/BitsTest.php @@ -4,8 +4,8 @@ namespace BitMask\Tests\Util; -use BitMask\Exception\InvalidIndexException; use BitMask\Exception\NotSingleBitException; +use BitMask\Exception\OutOfRangeException; use BitMask\Util\Bits; use PHPUnit\Framework\TestCase; @@ -18,31 +18,31 @@ class BitsTest extends TestCase { - public function testGetMostSignificantBit() + public function testGetMostSignificantBit(): void { assertEquals(0, Bits::getMostSignificantBit(0)); - assertEquals(1, Bits::getMostSignificantBit(1)); - assertEquals(4, Bits::getMostSignificantBit(8)); - assertEquals(4, Bits::getMostSignificantBit(15)); - /** @todo check PHP_INT_MAX */ -// assertEquals(4, Bits::getMSB(PHP_INT_MAX)); - /** check deprecated */ - assertEquals(0, Bits::getMSB(0)); + assertEquals(0, Bits::getMostSignificantBit(1)); + assertEquals(3, Bits::getMostSignificantBit(8)); + assertEquals(3, Bits::getMostSignificantBit(15)); + assertEquals(63, Bits::getMostSignificantBit(PHP_INT_MAX)); } - public function testGetSetBits() + public function testGetSetBits(): void { assertEquals([], Bits::getSetBits(0)); assertEquals([1, 2, 4], Bits::getSetBits(7)); + assertEquals([2, 16], Bits::getSetBits(0b10010)); + assertEquals([1], Bits::getSetBits(1)); // Util/Bits.php:31 [M] GreaterThanOrEqualTo + assertEquals([2], Bits::getSetBits(2)); // Util/Bits.php:32 [M] BitwiseAnd } - public function testIsSingleBit() + public function testIsSingleBit(): void { assertTrue(Bits::isSingleBit(8)); assertFalse(Bits::isSingleBit(7)); } - public function testBitToIndex() + public function testBitToIndex(): void { // single bit assertEquals(3, Bits::bitToIndex(8)); @@ -55,7 +55,7 @@ public function testBitToIndex() } } - public function testIndexToBit() + public function testIndexToBit(): void { // valid index assertEquals(8, Bits::indexToBit(3)); @@ -63,28 +63,28 @@ public function testIndexToBit() // invalid index try { Bits::indexToBit(-1); - } catch (InvalidIndexException $exception) { - assertSame('Index (zero based) must be greater than or equal to zero', $exception->getMessage()); + } catch (OutOfRangeException $exception) { + assertSame('-1', $exception->getMessage()); } } - public function testToString() + public function testToString(): void { assertEquals('111', Bits::toString(7)); } - public function testGetSetBitsIndexes() + public function testGetSetBitsIndexes(): void { assertEquals([0, 1, 2], Bits::getSetBitsIndexes(7)); } - public function testIsEvenNumber() + public function testIsEvenNumber(): void { assertTrue(Bits::isEvenNumber(2)); assertFalse(Bits::isEvenNumber(1)); } - public function testIsOddNumber() + public function testIsOddNumber(): void { assertTrue(Bits::isOddNumber(3)); assertFalse(Bits::isOddNumber(4)); diff --git a/tests/fixtures/Enum/BackedInt.php b/tests/fixtures/Enum/BackedInt.php new file mode 100644 index 0000000..a6d88b9 --- /dev/null +++ b/tests/fixtures/Enum/BackedInt.php @@ -0,0 +1,11 @@ +