QuasiRandom   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 423
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 37
eloc 105
c 5
b 0
f 0
dl 0
loc 423
ccs 118
cts 118
cp 1
rs 9.44

16 Methods

Rating   Name   Duplication   Size   Complexity  
A createQuasiSequence() 0 22 2
A internalNumberGenerator() 0 7 1
A resetQuasiIndexes() 0 4 1
A lookupSequence() 0 24 5
A getMaxNumber() 0 3 1
A getMinNumber() 0 3 1
A getInteger() 0 15 4
A getEightBits() 0 21 3
A __construct() 0 6 2
A getBool() 0 6 1
A getInt() 0 13 5
A setSeed() 0 32 5
A getBytes() 0 5 1
A generateSequences() 0 29 2
A validateSeedValue() 0 20 2
A __debugInfo() 0 7 1
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 given seed value and converts it to an integer.
98
     *
99
     * @param int|mixed $seed The initialization value.
100
     *
101
     * @return int The valid initialization value.
102
     * @throws \Exception Validation errors.
103
     */
104 1
    protected static function validateSeedValue($seed)
105
    {
106 1
        $seed = filter_var(
107 1
            $seed,
108 1
            FILTER_VALIDATE_INT,
109 1
            [
110 1
                "options" => [
111 1
                    "min_range" => self::QUASI_INT_MIN,
112 1
                    "max_range" => self::QUASI_INT_MAX,
113 1
                ],
114 1
            ]
115 1
        );
116
117 1
        if ($seed === false) {
118 1
            throw new \DomainException(
119 1
                "The provided seed value is of invalid type or is out of the supported range."
120 1
            );
121
        }
122
123 1
        return $seed;
124
    }
125
126
    /**
127
     * Reset internal pointers to the first element of each internal set.
128
     */
129 2
    protected static function resetQuasiIndexes()
130
    {
131 2
        self::$lastNumberIndex = 0;
132 2
        self::$lastByteIndex = 0;
133
    }
134
135
    /**
136
     * Generate an internal quasi-random numerical sequence.
137
     *
138
     * @param string $setName The internal static set name as a string.
139
     * @param int $seed The used seed value for the internal generator.
140
     * @param int $from The starting value in the range.
141
     * @param int $to The ending value in the range.
142
     * @param int $leapStep The leap step used.
143
     * @param int $skipStep The skip step used.
144
     */
145 2
    protected static function createQuasiSequence($setName, $seed, $from, $to, $leapStep, $skipStep)
146
    {
147
        // Set the seed value used for shuffling operations
148 2
        self::$seed = $seed;
149
150
        // Generate range with leap step
151 2
        self::${$setName} = range($from, $to, $leapStep);
152
153
        // Shuffle/scramble the numeric set
154 2
        $count = count(self::${$setName});
155 2
        $lastIndex = $count - 1;
156
157 2
        for ($currentIndex = 0; $currentIndex < $count; $currentIndex++) {
158 2
            $randomIndex = self::internalNumberGenerator(0, $lastIndex);
159
160 2
            $tmp = self::${$setName}[$currentIndex];
161 2
            self::${$setName}[$currentIndex] = self::${$setName}[$randomIndex];
162 2
            self::${$setName}[$randomIndex] = $tmp;
163
        }
164
165
        // Skip a few points
166 2
        array_splice(self::${$setName}, 0, $skipStep);
167
    }
168
169
    /**
170
     * Generate all needed quasi-random internal sequences.
171
     *
172
     * @param int $seed The used seed value for the internal generator.
173
     */
174 2
    protected static function generateSequences($seed)
175
    {
176
        // Calculate the skip step for usage
177 2
        $skipStep = (abs($seed) % 2 === 0) ? self::QUASI_SKIP_STEP : 0;
178
179
        // Generate range with leap step, from -32768 to 32767 => 16 bits
180 2
        self::createQuasiSequence(
181 2
            'randomNumbers',
182
            /** @see QuasiRandom::$randomNumbers */
183 2
            $seed,
184 2
            self::QUASI_INT_MIN,
185 2
            self::QUASI_INT_MAX,
186 2
            self::QUASI_LEAP_STEP,
187 2
            $skipStep
188 2
        );
189
190
        // Generate range with leap step, from 0 to 255 => 8 bits
191 2
        self::createQuasiSequence(
192 2
            'randomBytes',
193
            /** @see QuasiRandom::$randomBytes */
194 2
            $seed,
195 2
            0,
196 2
            255,
197 2
            self::QUASI_LEAP_STEP,
198 2
            $skipStep
199 2
        );
200
201
        // Reset to always start from the first element of both internal sets
202 2
        self::resetQuasiIndexes();
203
    }
204
205
    /**
206
     * Lookup method for searching in the internal integer set.
207
     *
208
     * @param int $minimum The lowest value to be returned.
209
     * @param int $maximum The highest value to be returned.
210
     *
211
     * @return int|null An proper integer number or `null` if not found.
212
     */
213 6
    protected static function lookupSequence($minimum, $maximum)
214
    {
215
        // Get the internal set size
216 6
        $internalCount = count(self::$randomNumbers);
217
218
        // If the whole sequence has been iterated at last call
219 6
        if (self::$lastNumberIndex === $internalCount) {
220 3
            self::$lastNumberIndex = 0; // Start over
221
        }
222
223
        // Lookup the sequence
224 6
        for ($i = self::$lastNumberIndex; $i < $internalCount; $i++) {
225
            // Update the lookup index and fetch the next number
226 6
            $number = self::$randomNumbers[$i];
227 6
            self::$lastNumberIndex = $i + 1;
228
229
            // If the number is in range, then return it
230 6
            if ($number >= $minimum && $number <= $maximum) {
231 6
                return $number;
232
            }
233
        }
234
235
        // Mark as not found on this iteration
236 3
        return null;
237
    }
238
239
    /**
240
     * Internal static method for single point consumption of the randomness source that outputs integers.
241
     *
242
     * @param int $minimum The lowest value to be returned.
243
     * @param int $maximum The highest value to be returned.
244
     *
245
     * @return int Randomly generated integer number.
246
     */
247 7
    protected static function getInteger($minimum, $maximum)
248
    {
249
        // Speed optimization for internal byte generation faster access
250 7
        if ($minimum === 0 && $maximum === 255) {
251 5
            $integer = unpack('C', self::getEightBits(1));
252
253 5
            return reset($integer); // Always an integer
254
        }
255
256
        do {
257
            // Searches for a proper number in the supported range
258 6
            $tmpNumber = self::lookupSequence($minimum, $maximum);
259 6
        } while ($tmpNumber === null);
260
261 6
        return $tmpNumber;
262
    }
263
264
    /**
265
     * Internal static method for single point consumption of the randomness source that outputs bytes.
266
     *
267
     * @param int $count The output string length based on the requested number of bytes.
268
     *
269
     * @return string Randomly generated string containing the requested number of bytes.
270
     */
271 7
    protected static function getEightBits($count)
272
    {
273 7
        $tmpBytes = '';
274
275 7
        $internalCount = count(self::$randomBytes);
276
277
        // Lookup the sequence
278 7
        for ($i = 1; $i <= $count; $i++) {
279
            // If the whole sequence has been iterated at last call
280 7
            if (self::$lastByteIndex === $internalCount) {
281 2
                self::$lastByteIndex = 0; // Start over
282
            }
283
284
            // Update the lookup index and fetch the next byte
285 7
            $byte = self::$randomBytes[self::$lastByteIndex];
286 7
            self::$lastByteIndex = self::$lastByteIndex + 1;
287
288 7
            $tmpBytes .= StringBuilder::getChr($byte);
289
        }
290
291 7
        return $tmpBytes;
292
    }
293
294
    /**
295
     * The maximum supported integer.
296
     *
297
     * @return int The upper integer generation border.
298
     */
299 15
    public function getMaxNumber()
300
    {
301 15
        return self::QUASI_INT_MAX;
302
    }
303
304
    /**
305
     * The minimum supported integer.
306
     *
307
     * @return int The lower integer generation border.
308
     */
309 16
    public function getMinNumber()
310
    {
311 16
        return self::QUASI_INT_MIN;
312
    }
313
314
    /**
315
     * The quasi-random generator constructor.
316
     *
317
     * Note: This type of generator is auto-seeded on the first object creation.
318
     */
319 30
    public function __construct()
320
    {
321 30
        parent::__construct();
322
323 30
        if (self::$seed === false) {
324 1
            self::setSeed();
325
        }
326
    }
327
328
    /**
329
     * Get debug information for the class instance.
330
     *
331
     * @return array Debug information.
332
     */
333 1
    public function __debugInfo()
334
    {
335 1
        return [
336 1
            'systemPrecision' => self::$systemPrecision,
337 1
            'quasiNumbersCount' => count(self::$randomNumbers),
338 1
            'quasiBytesCount' => count(self::$randomBytes),
339 1
            'seed' => self::$seed,
340 1
        ];
341
    }
342
343
    /**
344
     * Seed the generator initialization or invoke auto-seeding.
345
     *
346
     * Note: Invokes auto-seeding if the `null` value is passed.
347
     *
348
     * @param null|int $seed The initialization value.
349
     *
350
     * @throws \Exception Validation errors.
351
     */
352 2
    public static function setSeed($seed = null)
353
    {
354
        // If the seed is the same, just reset internal pointers
355 2
        if (!is_bool(self::$seed) && self::$seed === $seed) {
356 1
            self::resetQuasiIndexes();
357
358 1
            return;
359
        }
360
361
        // Seed the internal generator used for shuffling
362 2
        if (!is_null($seed)) {
363
            // Validate the input seed value
364 1
            $seed = self::validateSeedValue($seed);
365
366
            // Save original value before transformations
367 1
            $originalSeedValue = $seed;
368
369
            // Allow negative values and use them as initial for the internal shuffling
370 1
            $seed = ($seed < 0) ? QuasiRandom::QUASI_INT_MAX + 1 + (int)abs($seed) : (int)$seed;
371
        } else {
372
            // Get the seed value in the supported range
373 1
            $seed = (time() + 42) % (QuasiRandom::QUASI_INT_MAX + 1);
374
375
            // Save the auto-seed value
376 1
            $originalSeedValue = $seed;
377
        }
378
379
        // Generate internal sequences
380 2
        self::generateSequences($seed);
381
382
        // Set the used seed value for history
383 2
        self::$seed = $originalSeedValue;
384
    }
385
386
    /**
387
     * Generate a random integer number in a certain range.
388
     *
389
     * Note: Passing `null` will use the default parameter value.
390
     *
391
     * @param null|int $from The lowest value to be returned (default => 0).
392
     * @param null|int $to The highest value to be returned (default => $this->getMaxNumber()).
393
     *
394
     * @return int Randomly generated integer number.
395
     * @throws \Exception Validation errors.
396
     */
397 12
    public function getInt($from = 0, $to = null)
398
    {
399 12
        $from = ($from === null) ? 0 : $from;
400 12
        $to = ($to === null) ? $this->getMaxNumber() : $to;
401
402 12
        $this->validateIntegerRange($from, $to);
403
404
        // Speed optimization for boolean type generation
405 7
        if ($from === 0 && $to === 1) {
406 1
            return (int)$this->getBool();
407
        }
408
409 7
        return self::getInteger($from, $to);
410
    }
411
412
    /**
413
     * Generate a random byte string.
414
     *
415
     * Note: PHP represents bytes as characters to make byte strings.
416
     *
417
     * @param int $length The output string length (default => 1).
418
     *
419
     * @return string Randomly generated string containing the requested number of bytes.
420
     * @throws \Exception Validation errors.
421
     */
422 6
    public function getBytes($length = 1)
423
    {
424 6
        $this->validatePositiveInteger($length);
425
426 5
        return self::getEightBits($length);
427
    }
428
429
    /**
430
     * Generate a random boolean.
431
     *
432
     * @return bool Randomly generated boolean value.
433
     * @throws \Exception Validation errors.
434
     */
435 5
    public function getBool()
436
    {
437
        /**
438
         * {@internal Complete override because of the `$number % 2` is not with ~0.5 probability in the numeric set. }}
439
         */
440 5
        return self::getInteger(0, 255) > 127; // ~0.5 probability
441
    }
442
}
443