From f0bca4c34431a9be9c8a19425e45884faf350dfc Mon Sep 17 00:00:00 2001 From: Hichxm Date: Fri, 3 Jul 2026 22:55:23 +0200 Subject: [PATCH 1/7] refactor!: remove setters from HyperLogLog class The `setCounterBits`, `setHashAlgorithm`, and `setM` methods have been removed to make the class strict/immutable. BREAKING CHANGE: Consumers can no longer mutate the configuration of a HyperLogLog instance after creation. You must now pass these configurations during instantiation. --- src/HyperLogLog.php | 21 --------------------- tests/HyperLogLogTest.php | 9 --------- 2 files changed, 30 deletions(-) diff --git a/src/HyperLogLog.php b/src/HyperLogLog.php index eb0ca5e..082ff45 100644 --- a/src/HyperLogLog.php +++ b/src/HyperLogLog.php @@ -47,37 +47,16 @@ public function getCounterBits(): int return $this->counterBits; } - public function setCounterBits(int $counterBits): HyperLogLog - { - $this->counterBits = $counterBits; - - return $this; - } - public function getHashAlgorithm(): string { return $this->hashAlgorithm; } - public function setHashAlgorithm(string $hashAlgorithm): HyperLogLog - { - $this->hashAlgorithm = $hashAlgorithm; - - return $this; - } - public function getM(): int { return $this->m; } - public function setM(int $m): HyperLogLog - { - $this->m = $m; - - return $this; - } - /** * @return array */ diff --git a/tests/HyperLogLogTest.php b/tests/HyperLogLogTest.php index 15e8979..ad3bc69 100644 --- a/tests/HyperLogLogTest.php +++ b/tests/HyperLogLogTest.php @@ -38,15 +38,6 @@ public function testGettersAndSetters(): void { $hll = new HyperLogLog(); - $hll->setCounterBits(8); - $this->assertSame(8, $hll->getCounterBits()); - - $hll->setHashAlgorithm('md5'); - $this->assertSame('md5', $hll->getHashAlgorithm()); - - $hll->setM(256); - $this->assertSame(256, $hll->getM()); - $mockCounters = array_fill(0, 256, 1); $hll->setCounters($mockCounters); $this->assertSame($mockCounters, $hll->getCounters()); From 7683f1da53c81e3e9fd1b0f25f41bf577721e76f Mon Sep 17 00:00:00 2001 From: Hichxm Date: Fri, 3 Jul 2026 22:55:58 +0200 Subject: [PATCH 2/7] chore: add .phpunit.result.cache to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7bd006b..a8bc02a 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,5 @@ composer.lock .php-cs-fixer.cache .idea/ -.phpunit.cache \ No newline at end of file +.phpunit.cache +/.phpunit.result.cache From b08d83bab00fe9a211356d43c4e768ece98878e7 Mon Sep 17 00:00:00 2001 From: Hichxm Date: Fri, 3 Jul 2026 22:59:22 +0200 Subject: [PATCH 3/7] refactor: use class constant and self return type - Converted `$_precalculatedTwoPow32` property to `TWO_POW_32` class constant. - Updated `setCounters` return type hint from `HyperLogLog` to `self`. --- src/HyperLogLog.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/HyperLogLog.php b/src/HyperLogLog.php index 082ff45..71ab3d7 100644 --- a/src/HyperLogLog.php +++ b/src/HyperLogLog.php @@ -15,7 +15,7 @@ */ class HyperLogLog { - private int $_precalculatedTwoPow32 = 4_294_967_296; + private const TWO_POW_32 = 4_294_967_296; private int $counterBits; private string $hashAlgorithm; @@ -70,7 +70,7 @@ public function getCounters(): array * * @return $this */ - public function setCounters(array $counters): HyperLogLog + public function setCounters(array $counters): self { $this->counters = $counters; @@ -129,7 +129,7 @@ public function count(): float } // Large range correction - if ($E > $this->_precalculatedTwoPow32 / 30) { + if ($E > $this::TWO_POW_32 / 30) { $E = $this->estimateUsingLargeCardinalitiesApproach($E); } @@ -239,7 +239,7 @@ public function estimateUsingSmallCardinalitiesApproach(int $V, int $m): float */ public function estimateUsingLargeCardinalitiesApproach(float $E): float { - return -$this->_precalculatedTwoPow32 * log(1 - $E / $this->_precalculatedTwoPow32); + return -$this::TWO_POW_32 * log(1 - $E / $this::TWO_POW_32); } /** From 2546a25a5c42e3d4736a97e46b3013fe7a816680 Mon Sep 17 00:00:00 2001 From: Hichxm Date: Fri, 3 Jul 2026 23:19:07 +0200 Subject: [PATCH 4/7] perf: optimize bitwise operations using 64-bit integers - Replaced slow string-based byte/bit extraction in `counter` and `rho` with native 64-bit integer bitwise operations using `unpack('J')`. - Added padding logic (`$needsPadding`) to support hash algorithms producing less than 64 bits (e.g., crc32). - Expanded test suite significantly (invalid algorithms, empty strings, short hashes, and small/large range corrections). --- src/HyperLogLog.php | 70 ++++++++++++++------------ tests/HyperLogLogTest.php | 102 ++++++++++++++++++++++++++++++++++---- 2 files changed, 130 insertions(+), 42 deletions(-) diff --git a/src/HyperLogLog.php b/src/HyperLogLog.php index 71ab3d7..5090428 100644 --- a/src/HyperLogLog.php +++ b/src/HyperLogLog.php @@ -25,12 +25,15 @@ class HyperLogLog /** @var array Counters storing maximum rho values */ private array $counters; + /** @var bool Indicates if the selected hash algorithm produces less than 64 bits */ + private bool $needsPadding; + /** * HyperLogLog constructor. * * @param int $counterBits Number of bits used to define the number of counters (m = 2^counterBits). - * Higher values improve accuracy but increase memory usage. - * @param string $hashAlgorithm Hash algorithm used for input hashing (e.g. xxh3, murmur3f, sha256). + * Higher values improve accuracy but increase memory usage. + * @param string $hashAlgorithm Hash algorithm used for input hashing (e.g. xxh3, murmur3f, crc32, sha256). */ public function __construct(int $counterBits = 5, string $hashAlgorithm = 'xxh3') { @@ -40,6 +43,9 @@ public function __construct(int $counterBits = 5, string $hashAlgorithm = 'xxh3' $this->m = 1 << $this->counterBits; $this->counters = array_fill(0, $this->m, 0); + + $testHash = hash($this->hashAlgorithm, 'test', true); + $this->needsPadding = strlen($testHash) < 8; } public function getCounterBits(): int @@ -80,18 +86,27 @@ public function setCounters(array $counters): self /** * Add an element to the HyperLogLog structure. * - * The value is hashed, split into register index and leading zero count, - * and the register is updated with the maximum observed rho value. + * The value is hashed, converted to a 64-bit integer, split into register index + * and leading zero count, and the register is updated with the maximum observed rho value. * * @param string $value element to insert */ public function add(string $value): void { - $hash = $this->hash($value, $this->hashAlgorithm); + $hashString = $this->hash($value, $this->hashAlgorithm); + + // Comblement avec des octets nuls si le hachage fait moins de 64 bits (ex: crc32) + if ($this->needsPadding) { + $hashString = str_pad($hashString, 8, "\x00", STR_PAD_RIGHT); + } + + // Extraction des 8 premiers octets (64 bits) sous forme d'entier (Big Endian) + // unpack('J') ignore nativement tout ce qui dépasse 8 octets (ex: sha256, md5) + $hashInt = unpack('J', $hashString)[1]; - $counter = $this->counter($hash, $this->counterBits); + $counter = $this->counter($hashInt, $this->counterBits); - $rho = $this->rho($hash, $this->counterBits); + $rho = $this->rho($hashInt, $this->counterBits); if ($rho > $this->counters[$counter]) { $this->counters[$counter] = $rho; @@ -256,49 +271,40 @@ private function hash(string $value, string $hashAlgorithm): string } /** - * Extracts the register index from the hash. + * Extracts the register index from the 64-bit integer hash using bitwise shift. * - * The first `counterBits` bits of the hash determine the register. - * - * @param string $hash binary hash - * @param int $counterBits number of bits used for indexing + * @param int $hash 64-bit integer hash + * @param int $counterBits number of bits used for indexing * * @return int register index */ - private function counter(string $hash, int $counterBits): int + private function counter(int $hash, int $counterBits): int { - $counter = 0; - - for ($bit = 0; $bit < $counterBits; ++$bit) { - $byte = ord($hash[intdiv($bit, 8)]); + $shift = 64 - $counterBits; + $mask = (1 << $counterBits) - 1; - $counter = ($counter << 1) - | (($byte >> (7 - ($bit % 8))) & 1); - } - - return $counter; + return ($hash >> $shift) & $mask; } /** - * Computes the position of the first set bit (rho function). + * Computes the position of the first set bit (rho function) using bitwise shifts. * * Counts leading zeros starting after the register bits. * - * @param string $hash binary hash - * @param int $counterBits number of bits reserved for register selection + * @param int $hash 64-bit integer hash + * @param int $counterBits number of bits reserved for register selection * * @return int position of first 1-bit (rho value) */ - private function rho(string $hash, int $counterBits): int + private function rho(int $hash, int $counterBits): int { - $bitLength = strlen($hash) * 8; - $rho = 1; + $maxBitsToCheck = 64 - $counterBits; - for ($bit = $counterBits; $bit < $bitLength; ++$bit) { - $byte = ord($hash[intdiv($bit, 8)]); + for ($i = 0; $i < $maxBitsToCheck; ++$i) { + $bitPos = 63 - $counterBits - $i; - if (($byte >> (7 - ($bit % 8))) & 1) { + if (($hash >> $bitPos) & 1) { return $rho; } @@ -307,4 +313,4 @@ private function rho(string $hash, int $counterBits): int return $rho; } -} +} \ No newline at end of file diff --git a/tests/HyperLogLogTest.php b/tests/HyperLogLogTest.php index ad3bc69..23e2331 100644 --- a/tests/HyperLogLogTest.php +++ b/tests/HyperLogLogTest.php @@ -18,9 +18,14 @@ public function testConstructorSetsDefaultValues(): void { $hll = new HyperLogLog(); + // Check if the default counter bits and hash algorithms are set $this->assertSame(5, $hll->getCounterBits()); $this->assertSame('xxh3', $hll->getHashAlgorithm()); - $this->assertSame(32, $hll->getM()); // 1 << 5 = 32 + + // m should be 2^5 = 32 + $this->assertSame(32, $hll->getM()); + + // The counters array should be initialized with 32 elements set to 0 $this->assertCount(32, $hll->getCounters()); } @@ -30,16 +35,28 @@ public function testConstructorSetsCustomValues(): void $this->assertSame(10, $hll->getCounterBits()); $this->assertSame('sha256', $hll->getHashAlgorithm()); - $this->assertSame(1024, $hll->getM()); // 1 << 10 = 1024 + + // m should be 2^10 = 1024 + $this->assertSame(1024, $hll->getM()); $this->assertCount(1024, $hll->getCounters()); } + public function testConstructorFailsWithInvalidHashAlgorithm(): void + { + // In PHP 8+, the hash() function throws a ValueError if the provided algorithm does not exist. + $this->expectException(\ValueError::class); + new HyperLogLog(10, 'invalid_algo_123'); + } + public function testGettersAndSetters(): void { $hll = new HyperLogLog(); - $mockCounters = array_fill(0, 256, 1); + // Create a mock state to inject into the instance + $mockCounters = array_fill(0, 32, 1); $hll->setCounters($mockCounters); + + // Verify that the state was correctly updated $this->assertSame($mockCounters, $hll->getCounters()); } @@ -55,7 +72,7 @@ public function testAddAndCountUniqueElements(): void $estimate = $hll->count(); // With m=4096 (12 bits), the estimate should be highly accurate. - // We assert it falls within a reasonable probabilistic window (~5% error). + // We assert it falls within a reasonable probabilistic window (~5% error margin). $this->assertGreaterThan(4500, $estimate); $this->assertLessThan(5500, $estimate); } @@ -70,14 +87,75 @@ public function testAddDuplicateElementsMaintainsCardinality(): void $estimate = $hll->count(); - // Adding the exact same string 1000 times should result in a count of ~1 + // Adding the exact same string 1000 times should result in a total count of ~1 $this->assertEqualsWithDelta(1.0, $estimate, 1.0); } + public function testAddEmptyString(): void + { + $hll = new HyperLogLog(10, 'sha256'); + $hll->add(''); + + // An empty string is a valid element that should be processed and count as 1. + $this->assertEqualsWithDelta(1.0, $hll->count(), 1.0); + } + + public function testAddWithShortHashAlgorithmTriggersPaddingCorrectly(): void + { + // crc32 produces a 32-bit (4 bytes) hash. + // This must trigger the internal right-padding with null bytes so unpack('J') doesn't fail. + $hll = new HyperLogLog(10, 'crc32'); + + $hll->add('short_hash_test_1'); + $hll->add('short_hash_test_2'); + + $estimate = $hll->count(); + $this->assertGreaterThan(1.0, $estimate); + } + + public function testCountTriggersSmallRangeCorrectionInternalBranch(): void + { + $hll = new HyperLogLog(12, 'sha256'); // m = 4096 + + // By inserting only a few elements (e.g., 10), we ensure there are many empty registers ($V > 0) + // and that the raw estimate $E is <= (2.5 * 4096). + for ($i = 0; $i < 10; ++$i) { + $hll->add('small_range_item_'.$i); + } + + $estimate = $hll->count(); + + // Verify that the estimate remains consistent thanks to the linear counting correction. + $this->assertGreaterThan(5.0, $estimate); + $this->assertLessThan(15.0, $estimate); + } + + public function testCountTriggersLargeRangeCorrectionInternalBranch(): void + { + $hll = new HyperLogLog(10, 'sha256'); // m = 1024 + + // We use a simulated rho of 20 across all registers. + // The raw estimate $E will be approximately 773 million. + // This is well above the 143 million threshold (2^32 / 30) for large range correction, + // while remaining safely below the absolute 4.29 billion limit (2^32) to avoid logarithmic NAN errors. + $mockCounters = array_fill(0, 1024, 20); + $hll->setCounters($mockCounters); + + $estimate = $hll->count(); + $twoPow32 = 4_294_967_296; + + // Verify that the value is a valid float and not a mathematically undefined result (NAN). + $this->assertFalse(is_nan($estimate), 'The estimate must not be NAN.'); + + // Assert that the large range correction activated successfully and scaled the result appropriately. + $this->assertGreaterThan($twoPow32 / 30, $estimate); + } + public function testTheoreticalErrorRateCalculation(): void { $hll = new HyperLogLog(); + // Theoretical error rate formula: 1.04 / sqrt(m) // 1.04 / sqrt(16) = 1.04 / 4 = 0.26 $this->assertEqualsWithDelta(0.26, $hll->theoreticalErrorRate(16), 0.001); } @@ -96,6 +174,7 @@ public function testMeasureError(): void { $hll = new HyperLogLog(); + // measureError simply returns the difference: estimate - real $this->assertSame(5, $hll->measureError(15, 10)); $this->assertSame(-2, $hll->measureError(8, 10)); } @@ -104,10 +183,11 @@ public function testAlphaCalculationValues(): void { $hll = new HyperLogLog(); + // Test exact predefined constants for m=2 and m=16 $this->assertEqualsWithDelta(0.46852874309841, $hll->alpha(2), 0.0000001); $this->assertEqualsWithDelta(0.673, $hll->alpha(16), 0.001); - // Test default case calculation + // Test the default fallback calculation for m >= 128 (e.g., 512) $expectedLargeAlpha = 0.7213 / (1 + 1.079 / 512); $this->assertEqualsWithDelta($expectedLargeAlpha, $hll->alpha(512), 0.001); } @@ -127,6 +207,7 @@ public function testEstimateThrowsExceptionForInvalidZ(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid harmonic mean, $Z must be positive'); + // $Z (Harmonic mean) cannot be zero or negative $hll->estimate(16, 0.0); } @@ -134,9 +215,9 @@ public function testEstimateUsingLinearCounting(): void { $hll = new HyperLogLog(); $m = 64; - $v = 16; // 16 empty counters + $v = 16; // 16 empty counters out of 64 - // Linear Counting formula: m * log(m / V) + // Linear Counting formula for small cardinalities: m * log(m / V) $expected = 64 * log(64 / 16); // 64 * log(4) ≈ 88.72 $this->assertEqualsWithDelta($expected, $hll->estimateUsingLinearCounting($v, $m), 0.001); @@ -149,7 +230,8 @@ public function testEstimateUsingLargeCardinalitiesApproach(): void $rawEstimate = 3_000_000_000; $twoPow32 = 4_294_967_296; - // Large Cardinality formula: -2^32 * log(1 - E / 2^32) + // Large Cardinality formula to correct hash collision bias near 2^32: + // -2^32 * log(1 - E / 2^32) $expected = -$twoPow32 * log(1 - $rawEstimate / $twoPow32); $this->assertEqualsWithDelta( @@ -158,4 +240,4 @@ public function testEstimateUsingLargeCardinalitiesApproach(): void 0.001 ); } -} +} \ No newline at end of file From fc4bfc982b0844f78da608ef3dbca7862c04877f Mon Sep 17 00:00:00 2001 From: Hichxm Date: Fri, 3 Jul 2026 23:52:04 +0200 Subject: [PATCH 5/7] fix: explicitly validate hash algorithm on instantiation - Added a check using `hash_algos()` to ensure the provided hash algorithm is supported. - Throw `InvalidArgumentException` instead of relying on PHP's native `ValueError`. - Updated the corresponding unit test. --- src/HyperLogLog.php | 10 ++++++++++ tests/HyperLogLogTest.php | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/HyperLogLog.php b/src/HyperLogLog.php index 5090428..a66334b 100644 --- a/src/HyperLogLog.php +++ b/src/HyperLogLog.php @@ -4,6 +4,8 @@ namespace Hichxm\HyperLogLog; +use InvalidArgumentException; + /** * HyperLogLog implementation for approximate cardinality estimation. * @@ -28,6 +30,8 @@ class HyperLogLog /** @var bool Indicates if the selected hash algorithm produces less than 64 bits */ private bool $needsPadding; + private array $availableHashAlgorithms; + /** * HyperLogLog constructor. * @@ -37,6 +41,12 @@ class HyperLogLog */ public function __construct(int $counterBits = 5, string $hashAlgorithm = 'xxh3') { + $this->availableHashAlgorithms = hash_algos(); + + if (!in_array($hashAlgorithm, $this->availableHashAlgorithms)) { + throw new InvalidArgumentException('Invalid hash algorithm'); + } + $this->counterBits = $counterBits; $this->hashAlgorithm = $hashAlgorithm; diff --git a/tests/HyperLogLogTest.php b/tests/HyperLogLogTest.php index 23e2331..032c100 100644 --- a/tests/HyperLogLogTest.php +++ b/tests/HyperLogLogTest.php @@ -5,6 +5,7 @@ namespace Hichxm\HyperLogLog\Tests; use Hichxm\HyperLogLog\HyperLogLog; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -44,7 +45,7 @@ public function testConstructorSetsCustomValues(): void public function testConstructorFailsWithInvalidHashAlgorithm(): void { // In PHP 8+, the hash() function throws a ValueError if the provided algorithm does not exist. - $this->expectException(\ValueError::class); + $this->expectException(InvalidArgumentException::class); new HyperLogLog(10, 'invalid_algo_123'); } From eec84c8db0816552db1ddd3ae1dadb709dcd271c Mon Sep 17 00:00:00 2001 From: Hichxm Date: Fri, 3 Jul 2026 23:58:25 +0200 Subject: [PATCH 6/7] test: fix PHP 8.0 compatibility and inline hash_algos check - Updated test suite to fallback to `sha256` for PHP versions < 8.1 since `xxh3` is unavailable. - Refactored `HyperLogLog` constructor to inline `hash_algos()` and removed the unnecessary `$availableHashAlgorithms` state property. --- src/HyperLogLog.php | 6 +----- tests/HyperLogLogTest.php | 43 +++++++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/HyperLogLog.php b/src/HyperLogLog.php index a66334b..62256ab 100644 --- a/src/HyperLogLog.php +++ b/src/HyperLogLog.php @@ -30,8 +30,6 @@ class HyperLogLog /** @var bool Indicates if the selected hash algorithm produces less than 64 bits */ private bool $needsPadding; - private array $availableHashAlgorithms; - /** * HyperLogLog constructor. * @@ -41,9 +39,7 @@ class HyperLogLog */ public function __construct(int $counterBits = 5, string $hashAlgorithm = 'xxh3') { - $this->availableHashAlgorithms = hash_algos(); - - if (!in_array($hashAlgorithm, $this->availableHashAlgorithms)) { + if (!in_array($hashAlgorithm, hash_algos())) { throw new InvalidArgumentException('Invalid hash algorithm'); } diff --git a/tests/HyperLogLogTest.php b/tests/HyperLogLogTest.php index 032c100..4344d8f 100644 --- a/tests/HyperLogLogTest.php +++ b/tests/HyperLogLogTest.php @@ -17,11 +17,13 @@ class HyperLogLogTest extends TestCase { public function testConstructorSetsDefaultValues(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); // Check if the default counter bits and hash algorithms are set $this->assertSame(5, $hll->getCounterBits()); - $this->assertSame('xxh3', $hll->getHashAlgorithm()); + $this->assertSame($hashAlgorithm, $hll->getHashAlgorithm()); // m should be 2^5 = 32 $this->assertSame(32, $hll->getM()); @@ -51,7 +53,9 @@ public function testConstructorFailsWithInvalidHashAlgorithm(): void public function testGettersAndSetters(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); // Create a mock state to inject into the instance $mockCounters = array_fill(0, 32, 1); @@ -154,7 +158,9 @@ public function testCountTriggersLargeRangeCorrectionInternalBranch(): void public function testTheoreticalErrorRateCalculation(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); // Theoretical error rate formula: 1.04 / sqrt(m) // 1.04 / sqrt(16) = 1.04 / 4 = 0.26 @@ -163,7 +169,9 @@ public function testTheoreticalErrorRateCalculation(): void public function testTheoreticalErrorRateThrowsExceptionForInvalidM(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid number of counters, $m must be greater than 0'); @@ -173,7 +181,9 @@ public function testTheoreticalErrorRateThrowsExceptionForInvalidM(): void public function testMeasureError(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); // measureError simply returns the difference: estimate - real $this->assertSame(5, $hll->measureError(15, 10)); @@ -182,7 +192,9 @@ public function testMeasureError(): void public function testAlphaCalculationValues(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); // Test exact predefined constants for m=2 and m=16 $this->assertEqualsWithDelta(0.46852874309841, $hll->alpha(2), 0.0000001); @@ -195,7 +207,9 @@ public function testAlphaCalculationValues(): void public function testAlphaThrowsExceptionForInvalidM(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); $this->expectException(\InvalidArgumentException::class); $hll->alpha(0); @@ -203,7 +217,9 @@ public function testAlphaThrowsExceptionForInvalidM(): void public function testEstimateThrowsExceptionForInvalidZ(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid harmonic mean, $Z must be positive'); @@ -214,7 +230,9 @@ public function testEstimateThrowsExceptionForInvalidZ(): void public function testEstimateUsingLinearCounting(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); $m = 64; $v = 16; // 16 empty counters out of 64 @@ -227,7 +245,10 @@ public function testEstimateUsingLinearCounting(): void public function testEstimateUsingLargeCardinalitiesApproach(): void { - $hll = new HyperLogLog(); + $hashAlgorithm = PHP_VERSION >= 8100 ? 'xxh3' : 'sha256'; + + $hll = new HyperLogLog(hashAlgorithm: $hashAlgorithm); + $rawEstimate = 3_000_000_000; $twoPow32 = 4_294_967_296; From 7d438bc8f784b843035af4ebf9e38163a8887e05 Mon Sep 17 00:00:00 2001 From: Hichxm Date: Sat, 4 Jul 2026 00:06:42 +0200 Subject: [PATCH 7/7] fix: handle unpack failure and clean up code style - Added a safeguard in `add()` to throw an `\InvalidArgumentException` if `unpack()` returns `false`. - Removed `InvalidArgumentException` imports in favor of fully qualified class names (FQCN). - Added `@var` annotations to improve static analysis and IDE support. - Fixed docblock indentation and missing EOF newlines. --- src/HyperLogLog.php | 18 ++++++++++++------ tests/HyperLogLogTest.php | 5 ++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/HyperLogLog.php b/src/HyperLogLog.php index 62256ab..50442f3 100644 --- a/src/HyperLogLog.php +++ b/src/HyperLogLog.php @@ -4,8 +4,6 @@ namespace Hichxm\HyperLogLog; -use InvalidArgumentException; - /** * HyperLogLog implementation for approximate cardinality estimation. * @@ -17,6 +15,7 @@ */ class HyperLogLog { + /** @var int */ private const TWO_POW_32 = 4_294_967_296; private int $counterBits; @@ -34,13 +33,13 @@ class HyperLogLog * HyperLogLog constructor. * * @param int $counterBits Number of bits used to define the number of counters (m = 2^counterBits). - * Higher values improve accuracy but increase memory usage. + * Higher values improve accuracy but increase memory usage. * @param string $hashAlgorithm Hash algorithm used for input hashing (e.g. xxh3, murmur3f, crc32, sha256). */ public function __construct(int $counterBits = 5, string $hashAlgorithm = 'xxh3') { if (!in_array($hashAlgorithm, hash_algos())) { - throw new InvalidArgumentException('Invalid hash algorithm'); + throw new \InvalidArgumentException('Invalid hash algorithm'); } $this->counterBits = $counterBits; @@ -108,7 +107,14 @@ public function add(string $value): void // Extraction des 8 premiers octets (64 bits) sous forme d'entier (Big Endian) // unpack('J') ignore nativement tout ce qui dépasse 8 octets (ex: sha256, md5) - $hashInt = unpack('J', $hashString)[1]; + $hashIntUnpacked = unpack('J', $hashString); + + if (false === $hashIntUnpacked) { + throw new \InvalidArgumentException('Invalid hash value'); + } + + /** @var int $hashInt */ + $hashInt = $hashIntUnpacked[1]; $counter = $this->counter($hashInt, $this->counterBits); @@ -319,4 +325,4 @@ private function rho(int $hash, int $counterBits): int return $rho; } -} \ No newline at end of file +} diff --git a/tests/HyperLogLogTest.php b/tests/HyperLogLogTest.php index 4344d8f..daac9da 100644 --- a/tests/HyperLogLogTest.php +++ b/tests/HyperLogLogTest.php @@ -5,7 +5,6 @@ namespace Hichxm\HyperLogLog\Tests; use Hichxm\HyperLogLog\HyperLogLog; -use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -47,7 +46,7 @@ public function testConstructorSetsCustomValues(): void public function testConstructorFailsWithInvalidHashAlgorithm(): void { // In PHP 8+, the hash() function throws a ValueError if the provided algorithm does not exist. - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); new HyperLogLog(10, 'invalid_algo_123'); } @@ -262,4 +261,4 @@ public function testEstimateUsingLargeCardinalitiesApproach(): void 0.001 ); } -} \ No newline at end of file +}