Passed
Push — master ( 742b46...ae2e05 )
by Tony Karavasilev (Тони
20:10
created

QuasiRandom::setSeed()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 32
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 32
ccs 9
cts 9
cp 1
rs 9.5555
c 0
b 0
f 0
cc 5
nc 4
nop 1
crap 5
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
     * Validates the the given seed value and converts it to integer.
98
     *
99 2
     * @param mixed|int $seed The initialization value.
100
     *
101 2
     * @return int The valid initialization value.
102 2
     */
103 2
    protected static function validateSeedValue($seed)
104
    {
105
        $seed = filter_var(
106
            $seed,
107
            FILTER_VALIDATE_INT,
108
            [
109
                "options" => [
110
                    "min_range" => self::QUASI_INT_MIN,
111
                    "max_range" => self::QUASI_INT_MAX,
112
                ],
113
            ]
114
        );
115 2
116
        if ($seed === false) {
117
            throw new \DomainException(
118 2
                "The provided seed value is of invalid type or is out of the supported range."
119
            );
120
        }
121 2
122
        return $seed;
123
    }
124 2
125 2
    /**
126
     * Reset internal pointers to the first element of each internal set.
127 2
     */
128 2
    protected static function resetQuasiIndexes()
129
    {
130 2
        self::$lastNumberIndex = 0;
131 2
        self::$lastByteIndex = 0;
132 2
    }
133
134
    /**
135
     * Generate an internal quasi-random numerical sequence.
136 2
     *
137 2
     * @param string $setName The internal static set name as a string.
138
     * @param int $seed The used seed value for the internal generator.
139
     * @param int $from The starting value in the range.
140
     * @param int $to The ending value in the range.
141
     * @param int $leapStep The leap step used.
142
     * @param int $skipStep The skip step used.
143
     */
144
    protected static function createQuasiSequence($setName, $seed, $from, $to, $leapStep, $skipStep)
145
    {
146
        // Set the seed value used for shuffling operations
147 7
        self::$seed = $seed;
148
149
        // Generate range with leap step
150 7
        self::${$setName} = range($from, $to, $leapStep);
151 5
152
        // Shuffle/scramble the numeric set
153 5
        $count = count(self::${$setName});
154
        $lastIndex = $count - 1;
155
156 6
        for ($currentIndex = 0; $currentIndex < $count; $currentIndex++) {
157 6
            $randomIndex = self::internalNumberGenerator(0, $lastIndex);
158
159
            $tmp = self::${$setName}[$currentIndex];
160
            self::${$setName}[$currentIndex] = self::${$setName}[$randomIndex];
161 6
            self::${$setName}[$randomIndex] = $tmp;
162 3
        }
163
164
        // Skip a few points
165
        array_splice(self::${$setName}, 0, $skipStep);
166 6
    }
167
168 6
    /**
169 6
     * Generate all needed quasi-random internal sequences.
170
     *
171
     * @param int $seed The used seed value for the internal generator.
172 6
     */
173 6
    protected static function generateSequences($seed)
174
    {
175 6
        // Calculate the skip step for usage
176
        $skipStep = (abs($seed) % 2 === 0) ? self::QUASI_SKIP_STEP : 0;
177
178 3
        // Generate range with leap step, from -32768 to 32767 => 16 bits
179
        self::createQuasiSequence(
180 6
            'randomNumbers', /** @see QuasiRandom::$randomNumbers */
181
            $seed,
182
            self::QUASI_INT_MIN,
183
            self::QUASI_INT_MAX,
184
            self::QUASI_LEAP_STEP,
185
            $skipStep
186
        );
187
188
        // Generate range with leap step, from 0 to 255 => 8 bits
189
        self::createQuasiSequence(
190 7
            'randomBytes', /** @see QuasiRandom::$randomBytes */
191
            $seed,
192 7
            0,
193
            255,
194 7
            self::QUASI_LEAP_STEP,
195
            $skipStep
196
        );
197 7
198 7
        // Reset to always start from the first element of both internal sets
199 2
        self::resetQuasiIndexes();
200
    }
201
202
    /**
203 7
     * Lookup method for searching in the internal integer set.
204 7
     *
205
     * @param int $minimum The lowest value to be returned.
206 7
     * @param int $maximum The highest value to be returned.
207
     *
208
     * @return int|null An proper integer number or `null` if not found.
209 7
     */
210
    protected static function lookupSequence($minimum, $maximum)
211
    {
212
        // Get the internal set size
213
        $internalCount = count(self::$randomNumbers);
214
215
        // If the whole sequence has been iterated at last call
216
        if (self::$lastNumberIndex === $internalCount) {
217 15
            self::$lastNumberIndex = 0; // Start over
218
        }
219 15
220
        // Lookup the sequence
221
        for ($i = self::$lastNumberIndex; $i < $internalCount; $i++) {
222
            // Update the lookup index and fetch the next number
223
            $number = self::$randomNumbers[$i];
224
            self::$lastNumberIndex = $i + 1;
225
226
            // If the number is in range, then return it
227 16
            if ($number >= $minimum && $number <= $maximum) {
228
                return $number;
229 16
            }
230
        }
231
232
        // Mark as not found on this iteration
233
        return null;
234
    }
235
236
    /**
237 24
     * Internal static method for single point consumption of the randomness source that outputs integers.
238
     *
239 24
     * @param int $minimum The lowest value to be returned.
240
     * @param int $maximum The highest value to be returned.
241 24
     *
242 1
     * @return int Randomly generated integer number.
243
     */
244 24
    protected static function getInteger($minimum, $maximum)
245
    {
246
        // Speed optimization for internal byte generation faster access
247
        if ($minimum === 0 && $maximum === 255) {
248
            $integer = unpack('C', self::getEightBits(1));
249
250
            return reset($integer); // Always an integer
1 ignored issue
show
Bug introduced by
It seems like $integer can also be of type false; however, parameter $array of reset() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
            return reset(/** @scrutinizer ignore-type */ $integer); // Always an integer
Loading history...
251
        }
252
253 2
        do {
254
            // Searches for a proper number in the supported range
255
            $tmpNumber = self::lookupSequence($minimum, $maximum);
256 2
        } while ($tmpNumber === null);
257 1
258
        return $tmpNumber;
259 1
    }
260
261
    /**
262
     * Internal static method for single point consumption of the randomness source that outputs bytes.
263 2
     *
264 1
     * @param int $count The output string length based on the requested number of bytes.
265 1
     *
266 1
     * @return string Randomly generated string containing the requested number of bytes.
267
     */
268
    protected static function getEightBits($count)
269 1
    {
270 1
        $tmpBytes = '';
271
272
        $internalCount = count(self::$randomBytes);
273
274
        // Lookup the sequence
275 1
        for ($i = 1; $i <= $count; $i++) {
276 1
            // If the whole sequence has been iterated at last call
277 1
            if (self::$lastByteIndex === $internalCount) {
278
                self::$lastByteIndex = 0; // Start over
279
            }
280
281
            // Update the lookup index and fetch the next byte
282 1
            $byte = self::$randomBytes[self::$lastByteIndex];
283
            self::$lastByteIndex = self::$lastByteIndex + 1;
284
285 1
            $tmpBytes .= StringBuilder::getChr($byte);
286
        }
287
288 1
        return $tmpBytes;
289
    }
290
291 1
    /**
292
     * The maximum supported integer.
293
     *
294
     * @return int The upper integer generation border.
295 2
     */
296
    public function getMaxNumber()
297
    {
298 2
        return self::QUASI_INT_MAX;
299 2
    }
300 2
301 2
    /**
302 2
     * The minimum supported integer.
303 2
     *
304 2
     * @return int The lower integer generation border.
305
     */
306
    public function getMinNumber()
307
    {
308 2
        return self::QUASI_INT_MIN;
309 2
    }
310 2
311 2
    /**
312 2
     * The quasi-random generator constructor.
313 2
     *
314 2
     * Note: This type of generator is auto-seeded on the first object creation.
315
     */
316
    public function __construct()
317
    {
318 2
        parent::__construct();
319
320
        if (self::$seed === false) {
321 2
            self::setSeed();
322 2
        }
323
    }
324
325
    /**
326
     * Seed the generator initialization or invoke auto-seeding.
327
     *
328
     * Note: Invokes auto-seeding if the `null` value is passed.
329
     *
330
     * @param null|int $seed The initialization value.
331
     */
332
    public static function setSeed($seed = null)
333
    {
334
        // If the seed is the same, just reset internal pointers
335 12
        if (!is_bool(self::$seed) && self::$seed === $seed) {
336
            self::resetQuasiIndexes();
337 12
338 12
            return;
339
        }
340 12
341
        // Seed the internal generator used for shuffling
342
        if (!is_null($seed)) {
343 7
            // Validate the input seed value
344 1
            $seed = self::validateSeedValue($seed);
345
346
            // Save original value before transformations
347 7
            $originalSeedValue = $seed;
348
349
            // Allow negative values and use them as initial for the internal shuffling
350
            $seed = ($seed < 0) ? QuasiRandom::QUASI_INT_MAX + 1 + abs($seed) : (int)$seed;
351
        } else {
352
            // Get the seed value in the supported range
353
            $seed = (time() + 42) % (QuasiRandom::QUASI_INT_MAX + 1);
354
355
            // Save the auto-seed value
356
            $originalSeedValue = $seed;
357
        }
358
359
        // Generate internal sequences
360 6
        self::generateSequences($seed);
0 ignored issues
show
Bug introduced by
It seems like $seed can also be of type double; however, parameter $seed of CryptoManana\Randomness\...om::generateSequences() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

360
        self::generateSequences(/** @scrutinizer ignore-type */ $seed);
Loading history...
361
362 6
        // Set the used seed value for history
363
        self::$seed = $originalSeedValue;
364 5
    }
365
366
    /**
367
     * Generate a random integer number in a certain range.
368
     *
369
     * Note: Passing `null` will use the default parameter value.
370
     *
371
     * @param null|int $from The lowest value to be returned (default => 0).
372
     * @param null|int $to The highest value to be returned (default => $this->getMaxNumber()).
373 5
     *
374
     * @return int Randomly generated integer number.
375
     * @throws \Exception Validation errors.
376
     */
377
    public function getInt($from = 0, $to = null)
378 5
    {
379
        $from = ($from === null) ? 0 : $from;
380
        $to = ($to === null) ? $this->getMaxNumber() : $to;
381
382
        $this->validateIntegerRange($from, $to);
383
384
        // Speed optimization for boolean type generation
385
        if ($from === 0 && $to === 1) {
386 1
            return (int)$this->getBool();
387
        }
388 1
389 1
        return self::getInteger($from, $to);
390
    }
391 1
392 1
    /**
393 1
     * Generate a random byte string.
394
     *
395
     * Note: PHP represents bytes as characters to make byte strings.
396
     *
397
     * @param int $length The output string length (default => 1).
398
     *
399
     * @return string Randomly generated string containing the requested number of bytes.
400
     * @throws \Exception Validation errors.
401
     */
402
    public function getBytes($length = 1)
403
    {
404
        $this->validatePositiveInteger($length);
405
406
        return self::getEightBits($length);
407
    }
408
409
    /**
410
     * Generate a random boolean.
411
     *
412
     * @return bool Randomly generated boolean value.
413
     * @throws \Exception Validation errors.
414
     */
415
    public function getBool()
416
    {
417
        /**
418
         * {@internal Complete override because of the `$number % 2` is not with ~0.5 probability in the numeric set. }}
419
         */
420
        return self::getInteger(0, 255) > 127; // ~0.5 probability
421
    }
422
423
    /**
424
     * Get debug information for the class instance.
425
     *
426
     * @return array Debug information.
427
     */
428
    public function __debugInfo()
429
    {
430
        return array_merge(
431
            parent::__debugInfo(),
432
            [
433
                'quasiNumbersCount' => count(self::$randomNumbers),
434
                'quasiBytesCount' => count(self::$randomBytes),
435
                'seed' => self::$seed,
436
            ]
437
        );
438
    }
439
}
440