Chris@0: operators might accidentally let a float Chris@0: * through. Chris@0: */ Chris@0: Chris@0: try { Chris@16: /** @var int $min */ Chris@0: $min = RandomCompat_intval($min); Chris@0: } catch (TypeError $ex) { Chris@0: throw new TypeError( Chris@0: 'random_int(): $min must be an integer' Chris@0: ); Chris@0: } Chris@0: Chris@0: try { Chris@16: /** @var int $max */ Chris@0: $max = RandomCompat_intval($max); Chris@0: } catch (TypeError $ex) { Chris@0: throw new TypeError( Chris@0: 'random_int(): $max must be an integer' Chris@0: ); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Now that we've verified our weak typing system has given us an integer, Chris@0: * let's validate the logic then we can move forward with generating random Chris@0: * integers along a given range. Chris@0: */ Chris@0: if ($min > $max) { Chris@0: throw new Error( Chris@0: 'Minimum value must be less than or equal to the maximum value' Chris@0: ); Chris@0: } Chris@0: Chris@0: if ($max === $min) { Chris@12: return (int) $min; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Initialize variables to 0 Chris@0: * Chris@0: * We want to store: Chris@0: * $bytes => the number of random bytes we need Chris@0: * $mask => an integer bitmask (for use with the &) operator Chris@0: * so we can minimize the number of discards Chris@0: */ Chris@0: $attempts = $bits = $bytes = $mask = $valueShift = 0; Chris@16: /** @var int $attempts */ Chris@16: /** @var int $bits */ Chris@16: /** @var int $bytes */ Chris@16: /** @var int $mask */ Chris@16: /** @var int $valueShift */ Chris@0: Chris@0: /** Chris@0: * At this point, $range is a positive number greater than 0. It might Chris@0: * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to Chris@0: * a float and we will lose some precision. Chris@16: * Chris@16: * @var int|float $range Chris@0: */ Chris@0: $range = $max - $min; Chris@0: Chris@0: /** Chris@0: * Test for integer overflow: Chris@0: */ Chris@0: if (!is_int($range)) { Chris@0: Chris@0: /** Chris@0: * Still safely calculate wider ranges. Chris@0: * Provided by @CodesInChaos, @oittaa Chris@0: * Chris@0: * @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435 Chris@0: * Chris@0: * We use ~0 as a mask in this case because it generates all 1s Chris@0: * Chris@0: * @ref https://eval.in/400356 (32-bit) Chris@0: * @ref http://3v4l.org/XX9r5 (64-bit) Chris@0: */ Chris@0: $bytes = PHP_INT_SIZE; Chris@16: /** @var int $mask */ Chris@0: $mask = ~0; Chris@0: Chris@0: } else { Chris@0: Chris@0: /** Chris@0: * $bits is effectively ceil(log($range, 2)) without dealing with Chris@0: * type juggling Chris@0: */ Chris@0: while ($range > 0) { Chris@0: if ($bits % 8 === 0) { Chris@0: ++$bytes; Chris@0: } Chris@0: ++$bits; Chris@0: $range >>= 1; Chris@16: /** @var int $mask */ Chris@0: $mask = $mask << 1 | 1; Chris@0: } Chris@0: $valueShift = $min; Chris@0: } Chris@0: Chris@16: /** @var int $val */ Chris@0: $val = 0; Chris@0: /** Chris@0: * Now that we have our parameters set up, let's begin generating Chris@0: * random integers until one falls between $min and $max Chris@0: */ Chris@16: /** @psalm-suppress RedundantCondition */ Chris@0: do { Chris@0: /** Chris@0: * The rejection probability is at most 0.5, so this corresponds Chris@0: * to a failure probability of 2^-128 for a working RNG Chris@0: */ Chris@0: if ($attempts > 128) { Chris@0: throw new Exception( Chris@0: 'random_int: RNG is broken - too many rejections' Chris@0: ); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Let's grab the necessary number of random bytes Chris@0: */ Chris@0: $randomByteString = random_bytes($bytes); Chris@0: Chris@0: /** Chris@0: * Let's turn $randomByteString into an integer Chris@0: * Chris@0: * This uses bitwise operators (<< and |) to build an integer Chris@0: * out of the values extracted from ord() Chris@0: * Chris@0: * Example: [9F] | [6D] | [32] | [0C] => Chris@0: * 159 + 27904 + 3276800 + 201326592 => Chris@0: * 204631455 Chris@0: */ Chris@0: $val &= 0; Chris@0: for ($i = 0; $i < $bytes; ++$i) { Chris@0: $val |= ord($randomByteString[$i]) << ($i * 8); Chris@0: } Chris@16: /** @var int $val */ Chris@0: Chris@0: /** Chris@0: * Apply mask Chris@0: */ Chris@0: $val &= $mask; Chris@0: $val += $valueShift; Chris@0: Chris@0: ++$attempts; Chris@0: /** Chris@0: * If $val overflows to a floating point number, Chris@0: * ... or is larger than $max, Chris@0: * ... or smaller than $min, Chris@0: * then try again. Chris@0: */ Chris@0: } while (!is_int($val) || $val > $max || $val < $min); Chris@0: Chris@12: return (int) $val; Chris@0: } Chris@0: }