Skip to content

Commit 36fafdb

Browse files
committed
Possible fix for #61: 32-bit compatibility
1 parent 5268743 commit 36fafdb

14 files changed

+297
-98
lines changed

src/Matchers/BaseMatch.php

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace ZxcvbnPhp\Matchers;
66

77
use JetBrains\PhpStorm\ArrayShape;
8+
use ZxcvbnPhp\Math\Binomial;
89
use ZxcvbnPhp\Scorer;
910

1011
abstract class BaseMatch implements MatchInterface
@@ -119,46 +120,12 @@ public static function findAll(string $string, string $regex, int $offset = 0):
119120
*
120121
* @param int $n
121122
* @param int $k
122-
* @return int
123+
* @return float
124+
* @deprecated Use {@see Binomial::binom()} instead
123125
*/
124-
public static function binom(int $n, int $k): int
126+
public static function binom(int $n, int $k): float
125127
{
126-
if (function_exists('gmp_binomial')) {
127-
return gmp_intval(gmp_binomial($n, $k));
128-
}
129-
130-
return self::binomPolyfill($n, $k);
131-
}
132-
133-
/**
134-
* Substitute for gmp_polynomial for non-negative values of n and k.
135-
* @param int $n
136-
* @param int $k
137-
* @return int
138-
*/
139-
public static function binomPolyfill(int $n, int $k): int
140-
{
141-
if ($k < 0 || $n < 0) {
142-
throw new \DomainException("n and k must be non-negative");
143-
}
144-
145-
if ($k > $n) {
146-
return 0;
147-
}
148-
149-
// $k and $n - $k will always produce the same value, so use smaller of the two
150-
$k = min($k, $n - $k);
151-
152-
$c = 1;
153-
154-
for ($i = 1; $i <= $k; $i++, $n--) {
155-
// We're aiming for $c * $n / $i, but the $c * $n part could overflow, so use $c / $i * $n instead. The caveat here is that in
156-
// order to get a precise answer, we need to avoid floats, which means we need to deal with whole part and the remainder
157-
// separately.
158-
$c = intdiv($c, $i) * $n + intdiv($c % $i * $n, $i);
159-
}
160-
161-
return $c;
128+
return Binomial::binom($n, $k);
162129
}
163130

164131
abstract protected function getRawGuesses(): float;

src/Matchers/DictionaryMatch.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use JetBrains\PhpStorm\ArrayShape;
88
use ZxcvbnPhp\Matcher;
9+
use ZxcvbnPhp\Math\Binomial;
910

1011
class DictionaryMatch extends BaseMatch
1112
{
@@ -230,7 +231,7 @@ protected function getUppercaseVariations(): float
230231

231232
$variations = 0;
232233
for ($i = 1; $i <= min($uppercase, $lowercase); $i++) {
233-
$variations += static::binom($uppercase + $lowercase, $i);
234+
$variations += Binomial::binom($uppercase + $lowercase, $i);
234235
}
235236
return $variations;
236237
}

src/Matchers/L33tMatch.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use JetBrains\PhpStorm\ArrayShape;
88
use ZxcvbnPhp\Matcher;
9+
use ZxcvbnPhp\Math\Binomial;
910

1011
/**
1112
* Class L33tMatch extends DictionaryMatch to translate l33t into dictionary words for matching.
@@ -232,7 +233,7 @@ protected function getL33tVariations(): float
232233
} else {
233234
$possibilities = 0;
234235
for ($i = 1; $i <= min($subbed, $unsubbed); $i++) {
235-
$possibilities += static::binom($subbed + $unsubbed, $i);
236+
$possibilities += Binomial::binom($subbed + $unsubbed, $i);
236237
}
237238
$variations *= $possibilities;
238239
}

src/Matchers/SpatialMatch.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use JetBrains\PhpStorm\ArrayShape;
88
use ZxcvbnPhp\Matcher;
9+
use ZxcvbnPhp\Math\Binomial;
910

1011
class SpatialMatch extends BaseMatch
1112
{
@@ -236,7 +237,7 @@ protected function getRawGuesses(): float
236237
for ($i = 2; $i <= $length; $i++) {
237238
$possibleTurns = min($turns, $i - 1);
238239
for ($j = 1; $j <= $possibleTurns; $j++) {
239-
$guesses += static::binom($i - 1, $j - 1) * $startingPosition * pow($averageDegree, $j);
240+
$guesses += Binomial::binom($i - 1, $j - 1) * $startingPosition * pow($averageDegree, $j);
240241
}
241242
}
242243

@@ -251,7 +252,7 @@ protected function getRawGuesses(): float
251252
} else {
252253
$variations = 0;
253254
for ($i = 1; $i <= min($shifted, $unshifted); $i++) {
254-
$variations += static::binom($shifted + $unshifted, $i);
255+
$variations += Binomial::binom($shifted + $unshifted, $i);
255256
}
256257
$guesses *= $variations;
257258
}

src/Math/Binomial.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZxcvbnPhp\Math;
6+
7+
use ZxcvbnPhp\Math\Impl\BinomialProviderPhp73Gmp;
8+
use ZxcvbnPhp\Math\Impl\BinomialProviderFloat64;
9+
use ZxcvbnPhp\Math\Impl\BinomialProviderInt64;
10+
11+
class Binomial
12+
{
13+
private static $provider = null;
14+
15+
private function __construct()
16+
{
17+
throw new \LogicException(__CLASS__ . " is static");
18+
}
19+
20+
/**
21+
* Calculate binomial coefficient (n choose k).
22+
*
23+
* @param int $n
24+
* @param int $k
25+
* @return float
26+
*/
27+
public static function binom(int $n, int $k): float
28+
{
29+
return self::getProvider()->binom($n, $k);
30+
}
31+
32+
public static function getProvider(): BinomialProvider
33+
{
34+
if (self::$provider === null) {
35+
self::$provider = self::initProvider();
36+
}
37+
38+
return self::$provider;
39+
}
40+
41+
/**
42+
* @return string[]
43+
*/
44+
public static function getUsableProviderClasses(): array
45+
{
46+
// In order of priority. The first provider with a value of true will be used.
47+
$possibleProviderClasses = [
48+
BinomialProviderPhp73Gmp::class => function_exists('gmp_binomial'),
49+
BinomialProviderInt64::class => PHP_INT_SIZE >= 8,
50+
BinomialProviderFloat64::class => PHP_FLOAT_DIG >= 15,
51+
];
52+
53+
$possibleProviderClasses = array_filter($possibleProviderClasses);
54+
55+
return array_keys($possibleProviderClasses);
56+
}
57+
58+
private static function initProvider(): BinomialProvider
59+
{
60+
$providerClasses = self::getUsableProviderClasses();
61+
62+
if (!$providerClasses) {
63+
throw new \LogicException("No valid providers");
64+
}
65+
66+
$bestProviderClass = reset($providerClasses);
67+
68+
return new $bestProviderClass();
69+
}
70+
}

src/Math/BinomialProvider.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZxcvbnPhp\Math;
6+
7+
interface BinomialProvider
8+
{
9+
/**
10+
* Calculate binomial coefficient (n choose k).
11+
*
12+
* @param int $n
13+
* @param int $k
14+
* @return float
15+
*/
16+
public function binom(int $n, int $k): float;
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZxcvbnPhp\Math\Impl;
6+
7+
use ZxcvbnPhp\Math\BinomialProvider;
8+
9+
abstract class AbstractBinomialProvider implements BinomialProvider
10+
{
11+
public function binom(int $n, int $k): float
12+
{
13+
if ($k < 0 || $n < 0) {
14+
throw new \DomainException("n and k must be non-negative");
15+
}
16+
17+
if ($k > $n) {
18+
return 0;
19+
}
20+
21+
// $k and $n - $k will always produce the same value, so use smaller of the two
22+
$k = min($k, $n - $k);
23+
24+
return $this->calculate($n, $k);
25+
}
26+
27+
abstract protected function calculate(int $n, int $k): float;
28+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZxcvbnPhp\Math\Impl;
6+
7+
abstract class AbstractBinomialProviderWithFallback extends AbstractBinomialProvider
8+
{
9+
/**
10+
* @var AbstractBinomialProvider|null
11+
*/
12+
private $fallback = null;
13+
14+
protected function calculate(int $n, int $k): float
15+
{
16+
return $this->tryCalculate($n, $k) ?? $this->getFallbackProvider()->calculate($n, $k);
17+
}
18+
19+
abstract protected function tryCalculate(int $n, int $k): ?float;
20+
21+
abstract protected function initFallbackProvider(): AbstractBinomialProvider;
22+
23+
protected function getFallbackProvider(): AbstractBinomialProvider
24+
{
25+
if ($this->fallback === null) {
26+
$this->fallback = $this->initFallbackProvider();
27+
}
28+
29+
return $this->fallback;
30+
}
31+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZxcvbnPhp\Math\Impl;
6+
7+
class BinomialProviderFloat64 extends AbstractBinomialProvider
8+
{
9+
protected function calculate(int $n, int $k): float
10+
{
11+
$c = 1.0;
12+
13+
for ($i = 1; $i <= $k; $i++, $n--) {
14+
// We're aiming for $c * $n / $i, but the $c * $n part could cause us to lose precision, so use $c / $i * $n instead. The caveat
15+
// here is that in order to get a precise answer, we need to minimize the chances of going above ~2^52. This is mitigated
16+
// somewhat by dealing with whole part and the remainder separately, but it's not perfect and could overflow in practice, which
17+
// would result in a loss of precision.
18+
$c = floor($c / $i) * $n + floor(fmod($c, $i) * $n / $i);
19+
}
20+
21+
return $c;
22+
}
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ZxcvbnPhp\Math\Impl;
6+
7+
use TypeError;
8+
9+
class BinomialProviderInt64 extends AbstractBinomialProviderWithFallback
10+
{
11+
protected function initFallbackProvider(): AbstractBinomialProvider
12+
{
13+
return new BinomialProviderFloat64();
14+
}
15+
16+
protected function tryCalculate(int $n, int $k): ?float
17+
{
18+
try {
19+
$c = 1;
20+
21+
for ($i = 1; $i <= $k; $i++, $n--) {
22+
// We're aiming for $c * $n / $i, but the $c * $n part could overflow, so use $c / $i * $n instead. The caveat here is that in
23+
// order to get a precise answer, we need to avoid floats, which means we need to deal with whole part and the remainder
24+
// separately.
25+
$c = intdiv($c, $i) * $n + intdiv($c % $i * $n, $i);
26+
}
27+
28+
return (float)$c;
29+
} catch (TypeError $ex) {
30+
return null;
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)