├── LICENSE ├── README.md ├── composer.json ├── docker-compose.yaml └── src └── BloomFilter ├── BloomFilter.php ├── BloomFilterAbstract.php ├── CountingBloomFilter.php ├── DeletableFilter.php ├── DynamicBloomFilter.php ├── Exception ├── CannotRestore.php ├── InvalidCounter.php ├── InvalidValue.php ├── MaxLimitPerBitReached.php └── NotInitialized.php ├── Filter.php ├── Hash ├── Crc32b.php ├── Hash.php └── Murmur.php ├── Memento.php ├── Persist ├── BitPersister.php ├── BitRedis.php ├── BitString.php ├── CountPersister.php ├── CountRedis.php └── CountString15.php ├── Resetable.php └── Restorable.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (C) 2016, Rocket Internet SE 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Warning! 2 | 3 | Php 8 support is experimental. 4 | 5 | 6 | # PHP Bloom Filter 7 | 8 | A Bloom filter is a space-efficient probabilistic data structure, conceived by Burton Howard Bloom in 1970, 9 | that is used to test whether an element is a member of a set (False positive matches are possible, but false negatives are not). 10 | 11 | 12 | ```php 13 | 'localhost', 30 | 'port' => 6379, 31 | 'db' => 0, 32 | 'key' => 'bloom_filter', 33 | ]; 34 | $persisterRedis = Redis::create($redisParams); 35 | $persisterInRam = new BitString(); 36 | 37 | $filter = new BloomFilter($persisterRedis, new Murmur()); 38 | $filter->setSize(count($setToStore)); 39 | 40 | foreach ($setToStore as $string) { 41 | $filter->add($string); 42 | } 43 | 44 | if ($filter->has('Test string 1')) { 45 | echo 'Possibly in set"' . PHP_EOL; 46 | } else { 47 | echo 'Definitely not in set' . PHP_EOL; 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gfg/bloom-filter", 3 | "description": "Implementation of bloom filter", 4 | "authors": [ 5 | { 6 | "name": "Igor Veremchuk", 7 | "email": "igor.veremchuk@rocket-internet.de" 8 | } 9 | ], 10 | "require": { 11 | "php": "^7.2 || ^8.0" 12 | }, 13 | "require-dev": { 14 | "phpunit/phpunit": "^9.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "RocketLabs\\": "src/" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | php: 3 | user: 1000:1000 4 | build: 5 | dockerfile: .docker/Dockerfile 6 | target: php-dev-base 7 | context: . 8 | volumes: 9 | - ./:/var/app 10 | - ~/.composer:/var/.composer 11 | 12 | redis: 13 | image: redis:alpine 14 | -------------------------------------------------------------------------------- /src/BloomFilter/BloomFilter.php: -------------------------------------------------------------------------------- 1 | persister->setBulk($this->getBits($value)); 17 | 18 | return $this; 19 | } 20 | 21 | /** 22 | * @inheritdoc 23 | */ 24 | protected function doAddBulk(array $valueList) 25 | { 26 | $bits = []; 27 | foreach ($valueList as $value) { 28 | $bits[] = $this->getBits($value); 29 | } 30 | 31 | $this->persister->setBulk(call_user_func_array('array_merge', $bits)); 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * @inheritdoc 38 | */ 39 | protected function doHas(string $value): bool 40 | { 41 | $bits = $this->persister->getBulk($this->getBits($value)); 42 | 43 | return !in_array(0, $bits); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/BloomFilter/BloomFilterAbstract.php: -------------------------------------------------------------------------------- 1 | persister = $persister; 39 | $this->hash = $hash; 40 | $this->falsePositiveProbability = static::DEFAULT_PROBABILITY; 41 | $this->currentSetSize = 0; 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function has(string $value): bool 48 | { 49 | $this->assertInit(); 50 | 51 | return $this->doHas($value); 52 | } 53 | 54 | /** 55 | * @inheritdoc 56 | */ 57 | public function add(string $value) 58 | { 59 | $this->assertInit(); 60 | $this->currentSetSize++; 61 | 62 | return $this->doAdd($value); 63 | } 64 | 65 | /** 66 | * @inheritdoc 67 | */ 68 | public function addBulk(array $valueList) 69 | { 70 | $this->assertInit(); 71 | $this->currentSetSize += count($valueList); 72 | 73 | return $this->doAddBulk($valueList); 74 | } 75 | 76 | /** 77 | * @param string $value 78 | * @return bool 79 | */ 80 | abstract protected function doHas(string $value): bool; 81 | 82 | /** 83 | * @param array $valueList 84 | * @return $this 85 | */ 86 | abstract protected function doAddBulk(array $valueList); 87 | 88 | /** 89 | * @param string $value 90 | * @return $this 91 | */ 92 | abstract protected function doAdd(string $value); 93 | 94 | /** 95 | * @param int $setSize 96 | * 97 | * @return $this 98 | */ 99 | public function setSize(int $setSize) 100 | { 101 | $this->setSize = (int) $setSize; 102 | $this->init(); 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * @param float $falsePositiveProbability 109 | * 110 | * @return $this 111 | */ 112 | public function setFalsePositiveProbability(float $falsePositiveProbability) 113 | { 114 | if ($falsePositiveProbability <= 0 || $falsePositiveProbability >= 1) { 115 | throw new InvalidValue('False positive probability must be between 0 and 1'); 116 | } 117 | 118 | $this->falsePositiveProbability = $falsePositiveProbability; 119 | $this->init(); 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * @param int $currentSetSize 126 | * 127 | * @return $this 128 | */ 129 | public function setCurrentSetSize(int $currentSetSize) 130 | { 131 | $this->currentSetSize = $currentSetSize; 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * @inheritdoc 138 | */ 139 | public function reset() 140 | { 141 | $this->currentSetSize = 0; 142 | $this->persister->reset(); 143 | } 144 | 145 | /** 146 | * @inheritdoc 147 | */ 148 | public function saveState(): Memento 149 | { 150 | $this->assertInit(); 151 | $memento = new Memento(); 152 | $memento->setHashClass(get_class($this->hash)) 153 | ->addParam('setSize', $this->setSize) 154 | ->addParam('falsePositiveProbability', $this->falsePositiveProbability) 155 | ->addParam('currentSetSize', $this->currentSetSize); 156 | 157 | return $memento; 158 | } 159 | 160 | /** 161 | * @inheritdoc 162 | */ 163 | public function restoreState(Memento $memento) 164 | { 165 | $this->checkIntegrity($memento); 166 | $this->setSize($memento->getParam('setSize')); 167 | $this->setFalsePositiveProbability($memento->getParam('falsePositiveProbability')); 168 | $this->setCurrentSetSize($memento->getParam('currentSetSize')); 169 | $this->bitSize = $this->getOptimalBitSize($this->setSize, $this->falsePositiveProbability); 170 | $this->hashCount = $this->getOptimalHashCount($this->setSize, $this->bitSize); 171 | } 172 | 173 | /** 174 | * @param Memento $memento 175 | */ 176 | private function checkIntegrity(Memento $memento) 177 | { 178 | if ($memento->getHashClass() != get_class($this->hash)) { 179 | throw new CannotRestore('Memento object should have same hash class as object'); 180 | } 181 | 182 | if ($memento->getParam('setSize') === null) { 183 | throw new CannotRestore('Memento object has not "setSize" parameter'); 184 | } 185 | 186 | if ($memento->getParam('falsePositiveProbability') === null) { 187 | throw new CannotRestore('Memento object has not "falsePositiveProbability" parameter'); 188 | } 189 | 190 | if ($memento->getParam('currentSetSize') === null) { 191 | throw new CannotRestore('Memento object has not "currentSetSize" parameter'); 192 | } 193 | } 194 | 195 | /** 196 | * @return $this 197 | */ 198 | protected function init() 199 | { 200 | if (isset($this->setSize) && isset($this->falsePositiveProbability)) { 201 | $this->bitSize = $this->getOptimalBitSize($this->setSize, $this->falsePositiveProbability); 202 | $this->hashCount = $this->getOptimalHashCount($this->setSize, $this->bitSize); 203 | } 204 | 205 | return $this; 206 | } 207 | 208 | protected function assertInit() 209 | { 210 | if (!isset($this->setSize) || !isset($this->falsePositiveProbability)) { 211 | throw new NotInitialized(static::class . ' should be initialized'); 212 | } 213 | } 214 | 215 | /** 216 | * @param string $value 217 | * @param int $offset 218 | * @return array 219 | */ 220 | protected function getBits(string $value, int $offset = 0): array 221 | { 222 | $bits = []; 223 | 224 | for ($i = 0; $i < $this->hashCount; $i++) { 225 | $bits[] = $this->hash($value, $i); 226 | } 227 | 228 | if ($offset === 0) { 229 | return $bits; 230 | } else { 231 | return array_map( 232 | function($bit) use ($offset) { 233 | return $bit + ($offset * $this->bitSize); 234 | }, 235 | $bits 236 | ); 237 | } 238 | } 239 | 240 | /** 241 | * @param string $value 242 | * @param int $index 243 | * 244 | * @return int 245 | */ 246 | protected function hash(string $value, int $index) 247 | { 248 | return $this->hash->generate($value . $index) % $this->bitSize; 249 | } 250 | 251 | /** 252 | * m = ceil((n * log(p)) / log(1.0 / (pow(2.0, log(2.0))))); 253 | * m - Number of bits in the filter 254 | * n - Number of items in the filter 255 | * p - Probability of false positives, float between 0 and 1 or a number indicating 1-in-p 256 | * 257 | * @param int $setSize 258 | * @param float $falsePositiveProbability 259 | * @return int 260 | */ 261 | protected function getOptimalBitSize(int $setSize, float $falsePositiveProbability = 0.001): int 262 | { 263 | return (int) round((($setSize * log($falsePositiveProbability)) / pow(log(2), 2)) * -1); 264 | } 265 | 266 | /** 267 | * k = round(log(2.0) * m / n); 268 | * k - Number of hash functions 269 | * m - Number of bits in the filter 270 | * n - Number of items in the filter 271 | * 272 | * @param int $setSize 273 | * @param int $bitSize 274 | * @return int 275 | */ 276 | protected function getOptimalHashCount(int $setSize, int $bitSize): int 277 | { 278 | return (int) round(($bitSize / $setSize) * log(2)); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/BloomFilter/CountingBloomFilter.php: -------------------------------------------------------------------------------- 1 | countPersister = $countPersister; 25 | parent::__construct($bitPersister, $hash); 26 | } 27 | 28 | /** 29 | * @inheritdoc 30 | */ 31 | public function delete(string $value) 32 | { 33 | $this->currentSetSize--; 34 | $bits = $this->getBits($value); 35 | 36 | foreach ($bits as $bit) { 37 | $this->countPersister->decrementBit($bit); 38 | } 39 | $this->persister->unsetBulk($bits); 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function deleteBulk(array $valueList) 48 | { 49 | $bits = []; 50 | $this->currentSetSize -= count($valueList); 51 | foreach ($valueList as $value) { 52 | $bits[] = $this->getBits($value); 53 | } 54 | 55 | $bits = call_user_func_array('array_merge', $bits); 56 | 57 | foreach ($bits as $bit) { 58 | $this->countPersister->decrementBit($bit); 59 | } 60 | 61 | $this->persister->unsetBulk($bits); 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @inheritdoc 68 | */ 69 | protected function doAdd(string $value) 70 | { 71 | $bits = $this->getBits($value); 72 | foreach ($bits as $bit) { 73 | $this->countPersister->incrementBit($bit); 74 | } 75 | 76 | $this->persister->setBulk($bits); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * @inheritdoc 83 | */ 84 | protected function doAddBulk(array $valueList) 85 | { 86 | $bits = []; 87 | foreach ($valueList as $value) { 88 | $bits[] = $this->getBits($value); 89 | } 90 | 91 | $bits = call_user_func_array('array_merge', $bits); 92 | 93 | $this->persister->setBulk($bits); 94 | $this->countPersister->incrementBulk($bits); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @inheritdoc 101 | */ 102 | protected function doHas(string $value): bool 103 | { 104 | $bits = $this->persister->getBulk($this->getBits($value)); 105 | 106 | return !in_array(0, $bits); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/BloomFilter/DeletableFilter.php: -------------------------------------------------------------------------------- 1 | persister->setBulk($this->getBits($value, floor($this->currentSetSize / $this->setSize))); 16 | 17 | return $this; 18 | } 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | protected function doAddBulk(array $valueList) 24 | { 25 | $bits = []; 26 | $i = count($valueList); 27 | foreach ($valueList as $value) { 28 | $bits[] = $this->getBits($value, floor(($this->currentSetSize - $i--) / $this->setSize)); 29 | } 30 | $this->persister->setBulk(call_user_func_array('array_merge', $bits)); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | protected function doHas(string $value): bool 39 | { 40 | $this->assertInit(); 41 | $bloomFilterCount = floor($this->currentSetSize / $this->setSize); 42 | $result = false; 43 | $bits = $this->getBits($value); 44 | 45 | for ($i = 0; $i <= $bloomFilterCount; ++$i) { 46 | 47 | $result = !in_array( 48 | 0, 49 | $this->persister->getBulk(array_map( 50 | function($bit) use ($i) { 51 | return $bit + ($i * $this->bitSize); 52 | }, 53 | $bits 54 | ) 55 | ) 56 | ); 57 | 58 | if ($result) { 59 | return true; 60 | } 61 | } 62 | 63 | return $result; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/BloomFilter/Exception/CannotRestore.php: -------------------------------------------------------------------------------- 1 | = 0 ? $k1 >> 16 : (($k1 & 0x7fffffff) >> 16) | 0x8000)) * 0xcc9e2d51) & 0xffff) << 16))) & 0xffffffff; 25 | $k1 = $k1 << 15 | ($k1 >= 0 ? $k1 >> 17 : (($k1 & 0x7fffffff) >> 17) | 0x4000); 26 | $k1 = (((($k1 & 0xffff) * 0x1b873593) + ((((($k1 >= 0 ? $k1 >> 16 : (($k1 & 0x7fffffff) >> 16) | 0x8000)) * 0x1b873593) & 0xffff) << 16))) & 0xffffffff; 27 | $h1 ^= $k1; 28 | $h1 = $h1 << 13 | ($h1 >= 0 ? $h1 >> 19 : (($h1 & 0x7fffffff) >> 19) | 0x1000); 29 | $h1b = (((($h1 & 0xffff) * 5) + ((((($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000)) * 5) & 0xffff) << 16))) & 0xffffffff; 30 | $h1 = ((($h1b & 0xffff) + 0x6b64) + ((((($h1b >= 0 ? $h1b >> 16 : (($h1b & 0x7fffffff) >> 16) | 0x8000)) + 0xe654) & 0xffff) << 16)); 31 | } 32 | $k1 = 0; 33 | switch ($remainder) { 34 | case 3: $k1 ^= $value[$i + 2] << 16; 35 | case 2: $k1 ^= $value[$i + 1] << 8; 36 | case 1: $k1 ^= $value[$i]; 37 | $k1 = ((($k1 & 0xffff) * 0xcc9e2d51) + ((((($k1 >= 0 ? $k1 >> 16 : (($k1 & 0x7fffffff) >> 16) | 0x8000)) * 0xcc9e2d51) & 0xffff) << 16)) & 0xffffffff; 38 | $k1 = $k1 << 15 | ($k1 >= 0 ? $k1 >> 17 : (($k1 & 0x7fffffff) >> 17) | 0x4000); 39 | $k1 = ((($k1 & 0xffff) * 0x1b873593) + ((((($k1 >= 0 ? $k1 >> 16 : (($k1 & 0x7fffffff) >> 16) | 0x8000)) * 0x1b873593) & 0xffff) << 16)) & 0xffffffff; 40 | $h1 ^= $k1; 41 | } 42 | $h1 ^= $klen; 43 | $h1 ^= ($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000); 44 | $h1 = ((($h1 & 0xffff) * 0x85ebca6b) + ((((($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000)) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; 45 | $h1 ^= ($h1 >= 0 ? $h1 >> 13 : (($h1 & 0x7fffffff) >> 13) | 0x40000); 46 | $h1 = (((($h1 & 0xffff) * 0xc2b2ae35) + ((((($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000)) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; 47 | $h1 ^= ($h1 >= 0 ? $h1 >> 16 : (($h1 & 0x7fffffff) >> 16) | 0x8000); 48 | return $h1; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/BloomFilter/Memento.php: -------------------------------------------------------------------------------- 1 | hashClass = $hashClass; 22 | 23 | return $this; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getHashClass(): string 30 | { 31 | return $this->hashClass; 32 | } 33 | 34 | /** 35 | * @param string $key 36 | * @param $value 37 | * @return Memento 38 | */ 39 | public function addParam(string $key, $value): Memento 40 | { 41 | $this->params[$key] = $value; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * @param string $key 48 | * @return mixed|null 49 | */ 50 | public function getParam(string $key) 51 | { 52 | if (array_key_exists($key, $this->params)) { 53 | return $this->params[$key]; 54 | 55 | } 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/BloomFilter/Persist/BitPersister.php: -------------------------------------------------------------------------------- 1 | connect($host, $port); 35 | $redis->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_PHP); 36 | $redis->select($db); 37 | 38 | return new self($redis, $key); 39 | } 40 | 41 | /** 42 | * @param \Redis $redis 43 | * @param string $key 44 | */ 45 | public function __construct(\Redis $redis, $key) 46 | { 47 | $this->key = $key; 48 | $this->redis = $redis; 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public function reset() 55 | { 56 | $this->redis->del($this->key); 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function getBulk(array $bits): array 63 | { 64 | $pipe = $this->redis->pipeline(); 65 | 66 | foreach ($bits as $bit) { 67 | $this->assertOffset($bit); 68 | $pipe->getBit($this->key, $bit); 69 | } 70 | $return = $pipe->exec(); 71 | 72 | return is_array($return) ? $return : [$return]; 73 | } 74 | 75 | /** 76 | * @inheritdoc 77 | */ 78 | public function setBulk(array $bits) 79 | { 80 | $pipe = $this->redis->pipeline(); 81 | 82 | foreach ($bits as $bit) { 83 | $this->assertOffset($bit); 84 | $pipe->setBit($this->key, $bit, 1); 85 | } 86 | 87 | $pipe->exec(); 88 | } 89 | 90 | /** 91 | * @inheritdoc 92 | */ 93 | public function unsetBulk(array $bits) 94 | { 95 | $pipe = $this->redis->pipeline(); 96 | 97 | foreach ($bits as $bit) { 98 | $this->assertOffset($bit); 99 | $pipe->setBit($this->key, $bit, 0); 100 | } 101 | 102 | $pipe->exec(); 103 | } 104 | 105 | /** 106 | * @inheritdoc 107 | */ 108 | public function unset(int $bit) 109 | { 110 | $this->assertOffset($bit); 111 | $this->redis->setBit($this->key, $bit, 0); 112 | } 113 | 114 | /** 115 | * @inheritdoc 116 | */ 117 | public function get(int $bit): int 118 | { 119 | $this->assertOffset($bit); 120 | return $this->redis->getBit($this->key, $bit); 121 | } 122 | 123 | /** 124 | * @inheritdoc 125 | */ 126 | public function set(int $bit) 127 | { 128 | $this->assertOffset($bit); 129 | $this->redis->setBit($this->key, $bit, 1); 130 | } 131 | 132 | /** 133 | * @param int $value 134 | */ 135 | private function assertOffset(int $value) 136 | { 137 | if ($value < 0) { 138 | throw new InvalidValue('Value must be greater than zero.'); 139 | } 140 | } 141 | 142 | 143 | } -------------------------------------------------------------------------------- /src/BloomFilter/Persist/BitString.php: -------------------------------------------------------------------------------- 1 | bytes = $str; 27 | $instance->size = strlen($str); 28 | return $instance; 29 | } 30 | 31 | public function __construct() 32 | { 33 | $this->size = self::DEFAULT_BYTE_SIZE; 34 | $this->bytes = str_repeat(chr(0), $this->size); 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function reset() 41 | { 42 | $this->size = self::DEFAULT_BYTE_SIZE; 43 | $this->bytes = str_repeat(chr(0), $this->size); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function getBulk(array $bits): array 50 | { 51 | $resultBits = []; 52 | foreach ($bits as $bit) { 53 | $resultBits[] = $this->get($bit); 54 | } 55 | 56 | return $resultBits; 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function unsetBulk(array $bits) 63 | { 64 | foreach ($bits as $bit) { 65 | $this->unset($bit); 66 | } 67 | } 68 | 69 | public function unset(int $bit) 70 | { 71 | $offsetByte = $this->offsetToByte($bit); 72 | $byte = ord($this->bytes[$offsetByte]); 73 | 74 | $byte &= ~(1 << $bit % 8); 75 | $this->bytes[$offsetByte] = chr($byte); 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function setBulk(array $bits) 82 | { 83 | foreach ($bits as $bit) { 84 | $this->set($bit); 85 | } 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | public function get(int $bit): int 92 | { 93 | $byte = $this->offsetToByte($bit); 94 | $byte = ord($this->bytes[$byte]); 95 | 96 | return ($byte >> $bit % 8) & 1; 97 | } 98 | 99 | /** 100 | * @inheritdoc 101 | */ 102 | public function set(int $bit) 103 | { 104 | $offsetByte = $this->offsetToByte($bit); 105 | $byte = ord($this->bytes[$offsetByte]); 106 | 107 | $byte |= 1 << $bit % 8; 108 | $this->bytes[$offsetByte] = chr($byte); 109 | } 110 | 111 | /** 112 | * @param int $value 113 | */ 114 | private function assertOffset(int $value) 115 | { 116 | if ($value < 0) { 117 | throw new InvalidValue('Value must be greater than zero.'); 118 | } 119 | } 120 | 121 | /** 122 | * @param int $offset 123 | * @return int 124 | */ 125 | private function offsetToByte(int $offset): int 126 | { 127 | $this->assertOffset($offset); 128 | $byte = $offset >> 0x3; 129 | 130 | if ($this->size <= $byte) { 131 | $this->bytes .= str_repeat(chr(0), $byte - $this->size + self::DEFAULT_BYTE_SIZE); 132 | $this->size = strlen($this->bytes); 133 | } 134 | 135 | return $byte; 136 | } 137 | 138 | /** 139 | * @return string 140 | */ 141 | public function __toString(): string 142 | { 143 | return $this->bytes; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/BloomFilter/Persist/CountPersister.php: -------------------------------------------------------------------------------- 1 | connect($host, $port); 35 | $redis->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_PHP); 36 | $redis->select($db); 37 | 38 | return new self($redis, $key); 39 | } 40 | 41 | /** 42 | * @param \Redis $redis 43 | * @param string $key 44 | */ 45 | public function __construct(\Redis $redis, $key) 46 | { 47 | $this->key = $key; 48 | $this->redis = $redis; 49 | } 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public function reset() 55 | { 56 | $this->redis->del($this->key); 57 | } 58 | 59 | /** 60 | * @param int $bit 61 | * @return int 62 | */ 63 | public function decrementBit(int $bit): int 64 | { 65 | $result = $this->redis->hIncrBy($this->key, $bit, -1); 66 | if ($result < 0) { 67 | $this->redis->hSet($this->key, $bit, 0); 68 | throw new InvalidCounter( 69 | sprintf( 70 | 'Redis key [%s] had invalid count[%s] for the bit [%s]. Has been set to 0', 71 | $this->key, 72 | $result, 73 | $bit 74 | ) 75 | ); 76 | } 77 | 78 | return max([0, $result ]); 79 | } 80 | 81 | /** 82 | * @param int $bit 83 | * @return int 84 | */ 85 | public function incrementBit(int $bit): int 86 | { 87 | return $this->redis->hIncrBy($this->key, $bit, 1); 88 | } 89 | 90 | /** 91 | * @param array $bits 92 | * @return array 93 | */ 94 | public function incrementBulk(array $bits): array 95 | { 96 | $pipe = $this->redis->pipeline(); 97 | 98 | $result = []; 99 | 100 | foreach ($bits as $bit) { 101 | $result[$bit] = $pipe->hIncrBy($this->key, $bit, 1); 102 | } 103 | 104 | $pipe->exec(); 105 | 106 | return $result; 107 | } 108 | 109 | /** 110 | * @param int $bit 111 | * @return int 112 | */ 113 | public function get(int $bit): int 114 | { 115 | return $this->redis->hGet($this->key, $bit); 116 | } 117 | } -------------------------------------------------------------------------------- /src/BloomFilter/Persist/CountString15.php: -------------------------------------------------------------------------------- 1 | bytes = $str; 29 | $instance->size = strlen($str); 30 | return $instance; 31 | } 32 | 33 | public function __construct() 34 | { 35 | $this->size = self::DEFAULT_BYTE_SIZE; 36 | $this->bytes = str_repeat(chr(0), $this->size); 37 | } 38 | 39 | /** 40 | * @inheritdoc 41 | */ 42 | public function reset() 43 | { 44 | $this->size = self::DEFAULT_BYTE_SIZE; 45 | $this->bytes = str_repeat(chr(0), $this->size); 46 | } 47 | 48 | public function incrementBulk(array $bits): array 49 | { 50 | $result = []; 51 | 52 | foreach ($bits as $bit) { 53 | $result[$bit] = $this->incrementBit($bit); 54 | } 55 | 56 | return $result; 57 | } 58 | 59 | /** 60 | * @param int $bit 61 | * @return int 62 | */ 63 | public function incrementBit(int $bit): int 64 | { 65 | $offsetByte = $this->offsetToByte($bit); 66 | $byte = ord($this->bytes[$offsetByte]); 67 | 68 | $low = $byte & 0x0F; 69 | $high = ($byte >> 4) & 0x0F; 70 | 71 | if ($bit & 1) { 72 | $return = ++$high; 73 | } else { 74 | $return = ++$low; 75 | } 76 | 77 | if ($low > self::MAX_AMOUNT_PER_BIT || $high > self::MAX_AMOUNT_PER_BIT) { 78 | throw new MaxLimitPerBitReached('max amount per bit should not be higher than ' . self::MAX_AMOUNT_PER_BIT); 79 | } 80 | 81 | $this->bytes[$offsetByte] = chr($low | ($high << 4)); 82 | 83 | return $return; 84 | } 85 | 86 | /** 87 | * @param int $bit 88 | * @return int 89 | */ 90 | public function get(int $bit): int 91 | { 92 | $offsetByte = $this->offsetToByte($bit); 93 | $byte = ord($this->bytes[$offsetByte]); 94 | 95 | $low = $byte & 0x0F; 96 | $high = ($byte >> 4) & 0x0F; 97 | 98 | if ($bit & 1) { 99 | return $high; 100 | } else { 101 | return $low; 102 | } 103 | } 104 | 105 | /** 106 | * @param int $bit 107 | * @return int 108 | */ 109 | public function decrementBit(int $bit): int 110 | { 111 | $offsetByte = $this->offsetToByte($bit); 112 | $byte = ord($this->bytes[$offsetByte]); 113 | 114 | $low = $byte & 0x0F; 115 | $high = ($byte >> 4) & 0x0F; 116 | 117 | if ($bit & 1) { 118 | $return = --$high; 119 | } else { 120 | $return = --$low; 121 | } 122 | 123 | if ($low > self::MAX_AMOUNT_PER_BIT || $high > self::MAX_AMOUNT_PER_BIT) { 124 | throw new MaxLimitPerBitReached('max amount per bit should not be higher than ' . self::MAX_AMOUNT_PER_BIT); 125 | } 126 | 127 | $this->bytes[$offsetByte] = chr(max([$low, 0]) | (max([$high, 0]) << 4)); 128 | 129 | return max([$return, 0]); 130 | } 131 | 132 | /** 133 | * @param int $value 134 | */ 135 | private function assertOffset(int $value) 136 | { 137 | if ($value < 0) { 138 | throw new InvalidValue('Value must be greater than zero.'); 139 | } 140 | } 141 | 142 | /** 143 | * @param int $offset 144 | * @return int 145 | */ 146 | private function offsetToByte(int $offset): int 147 | { 148 | $this->assertOffset($offset); 149 | $byte = $offset / 2; 150 | 151 | if ($this->size <= $byte) { 152 | $this->bytes .= str_repeat(chr(0), $byte - $this->size + self::DEFAULT_BYTE_SIZE); 153 | $this->size = strlen($this->bytes); 154 | } 155 | 156 | return $byte; 157 | } 158 | 159 | /** 160 | * @return string 161 | */ 162 | public function __toString(): string 163 | { 164 | return $this->bytes; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/BloomFilter/Resetable.php: -------------------------------------------------------------------------------- 1 |