Passed
Push — master ( 742b46...c0a0b6 )
by Tony Karavasilev (Тони
01:29
created

QuasiRandom::validateSeedValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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