Passed
Push — master ( 855cb2...742b46 )
by Tony Karavasilev (Тони
01:26
created

QuasiRandom::getInteger()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 8

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 34
ccs 16
cts 16
cp 1
rs 8.4444
c 0
b 0
f 0
cc 8
nc 7
nop 2
crap 8
1
<?php
2
3
/**
4
 * The quasi-random generator class.
5
 */
6
7
namespace CryptoManana\Randomness;
8
9
use \CryptoManana\Core\Abstractions\Randomness\AbstractGenerator as RandomnessSource;
10
use \CryptoManana\Core\Interfaces\Randomness\SeedableGeneratorInterface as SeedAction;
11
use \CryptoManana\Core\StringBuilder as StringBuilder;
12
13
/**
14
 * Class QuasiRandom - The quasi-random generator object.
15
 *
16
 * @package CryptoManana\Randomness
17
 */
18
class QuasiRandom extends RandomnessSource implements SeedAction
19
{
20
    /**
21
     * The maximum supported integer.
22
     */
23
    const QUASI_INT_MAX = 32767;
24
25
    /**
26
     * The minimum supported integer.
27
     */
28
    const QUASI_INT_MIN = -32768;
29
30
    /**
31
     * The quasi algorithm default leap step.
32
     */
33
    const QUASI_LEAP_STEP = 1;
34
35
    /**
36
     * The quasi algorithm default skip step.
37
     */
38
    const QUASI_SKIP_STEP = 1;
39
40
    /**
41
     * The generated integer sequence property storage for all instances.
42
     *
43
     * @var array Quasi-random integer sequence.
44
     */
45
    protected static $randomNumbers = [];
46
47
    /**
48
     * The generated byte sequence property storage for all instances.
49
     *
50
     * @var array Quasi-random byte sequence as integers.
51
     */
52
    protected static $randomBytes = [];
53
54
    /**
55
     * Pointer to the next number from the quasi sequence.
56
     *
57
     * @uses QuasiRandom::$randomNumbers For fetching.
58
     *
59
     * @var int The next position for access.
60
     */
61
    protected static $lastNumberIndex = 0;
62
63
    /**
64
     * Pointer to the next byte from the quasi sequence.
65
     *
66
     * @uses QuasiRandom::$randomBytes For fetching.
67
     *
68
     * @var int The next position for access.
69
     */
70
    protected static $lastByteIndex = 0;
71
72
    /**
73
     * The initialization seed value property storage for all instances.
74
     *
75
     * @var bool|int The generator's seed value.
76
     */
77
    protected static $seed = false;
78
79
    /**
80
     * Custom pseudo-random number generator used for the shuffling of internal sets.
81
     *
82
     * @param int $minimum The lowest value to be returned.
83
     * @param int $maximum The highest value to be returned.
84
     *
85
     * @return int Randomly generated integer number.
86
     */
87 2
    protected static function internalNumberGenerator($minimum = 0, $maximum = 65535)
88
    {
89
        // Seed is multiplied by a prime number and divided by modulus of a prime number
90 2
        self::$seed = (self::$seed * 127) % 2976221 + 1;
91
92
        // Return number in the supported range
93 2
        return self::$seed % ($maximum - $minimum + 1) + $minimum;
94
    }
95
96
    /**
97
     * Reset internal pointers to the first element of each internal set.
98
     */
99 2
    protected static function resetQuasiIndexes()
100
    {
101 2
        self::$lastNumberIndex = 0;
102 2
        self::$lastByteIndex = 0;
103 2
    }
104
105
    /**
106
     * Generate an internal quasi-random numerical sequence.
107
     *
108
     * @param string $setName The internal static set name as a string.
109
     * @param int $seed The used seed value for the internal generator.
110
     * @param int $from The starting value in the range.
111
     * @param int $to The ending value in the range.
112
     * @param int $leapStep The leap step used.
113
     * @param int $skipStep The skip step used.
114
     */
115 2
    protected static function createQuasiSequence($setName, $seed, $from, $to, $leapStep, $skipStep)
116
    {
117
        // Set the seed value used for shuffling operations
118 2
        self::$seed = $seed;
119
120
        // Generate range with leap step
121 2
        self::${$setName} = range($from, $to, $leapStep);
122
123
        // Shuffle/scramble the numeric set
124 2
        $count = count(self::${$setName});
125 2
        $lastIndex = $count - 1;
126
127 2
        for ($currentIndex = 0; $currentIndex < $count; $currentIndex++) {
128 2
            $randomIndex = self::internalNumberGenerator(0, $lastIndex);
129
130 2
            $tmp = self::${$setName}[$currentIndex];
131 2
            self::${$setName}[$currentIndex] = self::${$setName}[$randomIndex];
132 2
            self::${$setName}[$randomIndex] = $tmp;
133
        }
134
135
        // Skip a few points
136 2
        array_splice(self::${$setName}, 0, $skipStep);
137 2
    }
138
139
    /**
140
     * Internal static method for single point consumption of the randomness source that outputs integers.
141
     *
142
     * @param int $minimum The lowest value to be returned.
143
     * @param int $maximum The highest value to be returned.
144
     *
145
     * @return int Randomly generated integer number.
146
     */
147 7
    protected static function getInteger($minimum, $maximum)
148
    {
149
        // Speed optimization for internal byte generation faster access
150 7
        if ($minimum === 0 && $maximum === 255) {
151 5
            $integer = unpack('C', self::getEightBits(1));
152
153 5
            return reset($integer);
154
        }
155
156 6
        $count = count(self::$randomNumbers);
157 6
        $tmpNumber = null;
158
159
        // Search for proper number in the supported range
160
        do {
161 6
            if (self::$lastNumberIndex === $count) {
162 3
                self::$lastNumberIndex = 0; // Start over
163
            }
164
165
            // Lookup the sequence
166 6
            for ($i = self::$lastNumberIndex; $i < $count; $i++) {
167
                // Update the lookup index and fetch the number
168 6
                $number = self::$randomNumbers[$i];
169 6
                self::$lastNumberIndex = $i + 1;
170
171
                // If the number is in range, then return it
172 6
                if ($number >= $minimum && $number <= $maximum) {
173 6
                    $tmpNumber = $number;
174
175 6
                    break 2; // Exit both loops faster
176
                }
177
            }
178 3
        } while ($tmpNumber === null);
179
180 6
        return $tmpNumber;
181
    }
182
183
    /**
184
     * Internal static method for single point consumption of the randomness source that outputs bytes.
185
     *
186
     * @param int $count The output string length based on the requested number of bytes.
187
     *
188
     * @return string Randomly generated string containing the requested number of bytes.
189
     */
190 7
    protected static function getEightBits($count)
191
    {
192 7
        $tmpBytes = '';
193
194 7
        $internalCount = count(self::$randomBytes);
195
196
        // Lookup the sequence
197 7
        for ($i = 1; $i <= $count; $i++) {
198 7
            if (self::$lastByteIndex === $internalCount) {
199 2
                self::$lastByteIndex = 0; // Start over
200
            }
201
202
            // Update the lookup index and fetch the next byte
203 7
            $byte = self::$randomBytes[self::$lastByteIndex];
204 7
            self::$lastByteIndex = self::$lastByteIndex + 1;
205
206 7
            $tmpBytes .= StringBuilder::getChr($byte);
207
        }
208
209 7
        return $tmpBytes;
210
    }
211
212
    /**
213
     * The maximum supported integer.
214
     *
215
     * @return int The upper integer generation border.
216
     */
217 15
    public function getMaxNumber()
218
    {
219 15
        return self::QUASI_INT_MAX;
220
    }
221
222
    /**
223
     * The minimum supported integer.
224
     *
225
     * @return int The lower integer generation border.
226
     */
227 16
    public function getMinNumber()
228
    {
229 16
        return self::QUASI_INT_MIN;
230
    }
231
232
    /**
233
     * The quasi-random generator constructor.
234
     *
235
     * Note: This type of generator is auto-seeded on the first object creation.
236
     */
237 24
    public function __construct()
238
    {
239 24
        parent::__construct();
240
241 24
        if (self::$seed === false) {
242 1
            self::setSeed();
243
        }
244 24
    }
245
246
    /**
247
     * Seed the generator initialization or invoke auto-seeding.
248
     *
249
     * Note: Invokes auto-seeding if the `null` value is passed.
250
     *
251
     * @param null|int $seed The initialization value.
252
     */
253 2
    public static function setSeed($seed = null)
254
    {
255
        // If the seed is the same, just reset internal counters
256 2
        if (!is_bool(self::$seed) && self::$seed === $seed) {
257 1
            self::resetQuasiIndexes();
258
259 1
            return;
260
        }
261
262
        // Seed the internal generator used for shuffling
263 2
        if (!is_null($seed)) {
264 1
            $seed = filter_var(
265 1
                $seed,
266 1
                FILTER_VALIDATE_INT,
267
                [
268
                    "options" => [
269 1
                        "min_range" => self::QUASI_INT_MIN,
270 1
                        "max_range" => self::QUASI_INT_MAX,
271
                    ],
272
                ]
273
            );
274
275 1
            if ($seed === false) {
276 1
                throw new \DomainException(
277 1
                    "The provided seed value is of invalid type or is out of the supported range."
278
                );
279
            }
280
281
            // Save original value before transformations
282 1
            $originalSeedValue = $seed;
283
284
            // Allow negative values and use them as initial for the internal shuffling
285 1
            $seed = ($seed < 0) ? QuasiRandom::QUASI_INT_MAX + abs($seed) : (int)$seed;
286
        } else {
287
            // Get the seed value in the supported range
288 1
            $seed = (time() + 42) % (QuasiRandom::QUASI_INT_MAX + 1);
289
290
            // Save the auto-seed value
291 1
            $originalSeedValue = $seed;
292
        }
293
294
        // Calculate the skip step for usage
295 2
        $skipStep = (abs($originalSeedValue) % 2 === 0) ? self::QUASI_SKIP_STEP : 0;
296
297
        // Generate range with leap step, from -32768 to 32767 => 16 bits
298 2
        self::createQuasiSequence(
299 2
            'randomNumbers', /** @see QuasiRandom::$randomNumbers */
300 2
            $seed,
301 2
            self::QUASI_INT_MIN,
302 2
            self::QUASI_INT_MAX,
303 2
            self::QUASI_LEAP_STEP,
304 2
            $skipStep
305
        );
306
307
        // Generate range with leap step, from 0 to 255 => 8 bits
308 2
        self::createQuasiSequence(
309 2
            'randomBytes', /** @see QuasiRandom::$randomBytes */
310 2
            $seed,
311 2
            0,
312 2
            255,
313 2
            self::QUASI_LEAP_STEP,
314 2
            $skipStep
315
        );
316
317
        // Reset to always start from the first element of both internal sets
318 2
        self::resetQuasiIndexes();
319
320
        // Set the used seed value for history
321 2
        self::$seed = $originalSeedValue;
322 2
    }
323
324
    /**
325
     * Generate a random integer number in a certain range.
326
     *
327
     * Note: Passing `null` will use the default parameter value.
328
     *
329
     * @param null|int $from The lowest value to be returned (default => 0).
330
     * @param null|int $to The highest value to be returned (default => $this->getMaxNumber()).
331
     *
332
     * @return int Randomly generated integer number.
333
     * @throws \Exception Validation errors.
334
     */
335 12
    public function getInt($from = 0, $to = null)
336
    {
337 12
        $from = ($from === null) ? 0 : $from;
338 12
        $to = ($to === null) ? $this->getMaxNumber() : $to;
339
340 12
        $this->validateIntegerRange($from, $to);
341
342
        // Speed optimization for boolean type generation
343 7
        if ($from === 0 && $to === 1) {
344 1
            return (int)$this->getBool();
345
        }
346
347 7
        return self::getInteger($from, $to);
348
    }
349
350
    /**
351
     * Generate a random byte string.
352
     *
353
     * Note: PHP represents bytes as characters to make byte strings.
354
     *
355
     * @param int $length The output string length (default => 1).
356
     *
357
     * @return string Randomly generated string containing the requested number of bytes.
358
     * @throws \Exception Validation errors.
359
     */
360 6
    public function getBytes($length = 1)
361
    {
362 6
        $this->validatePositiveInteger($length);
363
364 5
        return self::getEightBits($length);
365
    }
366
367
    /**
368
     * Generate a random boolean.
369
     *
370
     * @return bool Randomly generated boolean value.
371
     * @throws \Exception Validation errors.
372
     */
373 5
    public function getBool()
374
    {
375
        /**
376
         * {@internal Complete override because of the `$number % 2` is not with ~0.5 probability in the numeric set. }}
377
         */
378 5
        return self::getInteger(0, 255) > 127; // ~0.5 probability
379
    }
380
381
    /**
382
     * Get debug information for the class instance.
383
     *
384
     * @return array Debug information.
385
     */
386 1
    public function __debugInfo()
387
    {
388 1
        return array_merge(
389 1
            parent::__debugInfo(),
390
            [
391 1
                'quasiNumbersCount' => count(self::$randomNumbers),
392 1
                'quasiBytesCount' => count(self::$randomBytes),
393 1
                'seed' => self::$seed,
394
            ]
395
        );
396
    }
397
}
398