Passed
Branch master (df288f)
by Tony Karavasilev (Тони
02:30
created

QuasiRandom   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 420
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 420
ccs 113
cts 113
cp 1
rs 9.44

16 Methods

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