├── .gitignore ├── .travis.yml ├── bin ├── pure-random.js ├── xorshift128.c └── compare-test.sh ├── package.json ├── LICENSE ├── src └── pure-random.js ├── README.md └── test └── pure-random.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin/xorshift 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | -------------------------------------------------------------------------------- /bin/pure-random.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const rnd = require('../src/pure-random') 3 | const seed = process.argv.slice(2).map(string => parseInt(string)) 4 | 5 | console.log(rnd.randUint(seed)) 6 | process.exit(0) 7 | -------------------------------------------------------------------------------- /bin/xorshift128.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | // Marsaglia, George (July 2003). "Xorshift RNGs". Journal of Statistical Software 8 (14). 6 | uint32_t x, y, z, w; 7 | 8 | uint32_t xorshift128(void) { 9 | uint32_t t = x; 10 | t ^= t << 11; 11 | t ^= t >> 8; 12 | x = y; y = z; z = w; 13 | w ^= w >> 19; 14 | w ^= t; 15 | return w; 16 | } 17 | 18 | 19 | int main(int argc, char *argv[]) { 20 | x = strtol (argv[1], NULL, 0); 21 | y = strtol (argv[2], NULL, 0); 22 | z = strtol (argv[3], NULL, 0); 23 | w = strtol (argv[4], NULL, 0); 24 | 25 | printf("%u\n", xorshift128()); 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pure-random", 3 | "version": "0.1.1", 4 | "description": "A purely functional random number generator", 5 | "main": "src/pure-random.js", 6 | "scripts": { 7 | "pretest": "gcc -o bin/xorshift bin/xorshift128.c", 8 | "test": "mocha && bin/compare-test.sh", 9 | "clean": "rm -f bin/xorshift" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+ssh://git@github.com/Risto-Stevcev/pure-random.git" 14 | }, 15 | "keywords": [ 16 | "random", 17 | "number", 18 | "generator", 19 | "pure", 20 | "fantayland", 21 | "fantasy", 22 | "land", 23 | "csprng" 24 | ], 25 | "author": "Risto Stevcev", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/Risto-Stevcev/pure-random/issues" 29 | }, 30 | "homepage": "https://github.com/Risto-Stevcev/pure-random#readme", 31 | "dependencies": { 32 | "ramda": "^0.19.1", 33 | "sanctuary": "^0.9.0" 34 | }, 35 | "devDependencies": { 36 | "chai": "^3.5.0", 37 | "jsverify": "^0.7.1", 38 | "mocha": "^2.4.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Risto Stevcev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pure-random.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const R = require('ramda') 3 | , S = require('sanctuary') 4 | 5 | const UINT32_MIN = 0 6 | const UINT32_MAX = 4294967295 7 | 8 | // Marsaglia, George (July 2003). "Xorshift RNGs". Journal of Statistical Software 8 (14). 9 | // :: [UInt32] -> UInt32 10 | const xorshift = seed => { 11 | var x = seed[0], y = seed[1], z = seed[2], w = seed[3]; 12 | var t = x; 13 | t = (t ^ (t << 11)) >>> 0; 14 | t = (t ^ (t >>> 8)) >>> 0; 15 | x = y; y = z; z = w; 16 | w = (w ^ (w >>> 19)) >>> 0; 17 | w = (w ^ t) >>> 0; 18 | return w; 19 | } 20 | 21 | // :: String -> Number 22 | const fromHex = hex => parseInt(hex, 16) 23 | 24 | // :: () -> [UInt32] 25 | const genCsSeed = () => { 26 | const seed = crypto.randomBytes(128 / 8).toString('hex') 27 | return R.map(fromHex, [seed.slice(0, 8), seed.slice(8, 16), seed.slice(16, 24), seed.slice(24, 32)]) 28 | } 29 | 30 | // :: () -> [UInt32] 31 | const genSeed = () => { 32 | now = Date.now() 33 | x = now % UINT32_MAX 34 | y = now << now >>> 0 % UINT32_MAX 35 | z = y * 11 % UINT32_MAX 36 | w = x * now % UINT32_MAX 37 | return [x,y,z,w] 38 | } 39 | 40 | // :: Num -> Boolean 41 | const inRange = R.both(R.lte(UINT32_MIN), R.gte(UINT32_MAX)) 42 | 43 | // :: [UInt32] -> Int -> Int -> ([Uint32] -> Int -> Int) 44 | const validate = (seed, min, max, randFn) => { 45 | if (!R.all(R.both(Number.isInteger, inRange), seed)) 46 | return S.Left(TypeError(`Seed must be an array of four integers between [${UINT32_MIN}, ${UINT32_MAX}]`)) 47 | else if (typeof min !== 'number') 48 | return S.Left(TypeError('Min must be a number')) 49 | else if (typeof max !== 'number') 50 | return S.Left(TypeError('Max must be a number')) 51 | else if (min >= max) 52 | return S.Left(Error('Min must be less than max')) 53 | else 54 | return S.Right(randFn(seed, min, max)) 55 | } 56 | 57 | // :: [UInt32] -> Int -> Int -> Float 58 | const rand = R.curry((seed, min, max) => min + xorshift(seed) / UINT32_MAX * (max - min)) 59 | 60 | // :: [UInt32] -> Int -> Int -> Either Error Float 61 | const randFloat = R.curry((seed, min, max) => validate(seed, min, max, rand)) 62 | 63 | // :: [UInt32] -> Int -> Int -> Either Error Int 64 | const random = R.curry((seed, min, max) => randFloat(seed, min, max).map(Math.round)) 65 | 66 | module.exports = { 67 | genSeed: genSeed, 68 | genCsSeed: genCsSeed, 69 | 70 | randUint: xorshift, 71 | randFloat: randFloat, 72 | random: random, 73 | } 74 | 75 | /* 76 | Rules for bitwise operations on 32-bit unsigned ints 77 | 1. always end bit wise ops with ">>> 0" so the result gets interpreted as unsigned. 78 | 2. don't use >>. If the left-most bit is 1 it will try to preseve the sign and thus will introduce 1's to the left. 79 | Always use >>>. 80 | Source: http://stackoverflow.com/questions/6798111/bitwise-operations-on-32-bit-unsigned-ints 81 | */ 82 | -------------------------------------------------------------------------------- /bin/compare-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | test() { 4 | cresult=$(./bin/xorshift $1 $2 $3 $4) 5 | jsresult=$(./bin/pure-random.js $1 $2 $3 $4) 6 | 7 | if [[ "$actual" == "$expected" ]] ; then 8 | echo "Passed: [$1, $2, $3, $4]" 9 | else 10 | echo "Failed: [$1, $2, $3, $4]" 11 | fi 12 | } 13 | 14 | test 4094795843 75119171 1064088777 3185678737 15 | test 3422061919 4144178289 712208699 891699853 16 | test 2421563814 986280361 797486985 152627892 17 | test 1283482003 4119068132 993830538 2588009839 18 | test 1602239583 1618661217 2560587399 3031116569 19 | test 684504637 4234986933 4179878701 2262970688 20 | test 1814106503 2472046861 1918473817 772868280 21 | test 359749222 1602057036 3000252122 3138640507 22 | test 1840739379 3126610467 3678357570 951859248 23 | test 769272263 3039900221 313663903 3912870906 24 | test 3800932039 2537613697 1692656038 2295829955 25 | test 231008538 1468863555 2388779991 3828885615 26 | test 259977820 3762568731 1210134295 2926408437 27 | test 3090372046 517997927 2444043940 3485139545 28 | test 90287652 636741466 4252948589 3751809656 29 | test 4235477692 3159244831 2835050514 448021155 30 | test 2884904461 2181831758 1270899699 3971124756 31 | test 1084890439 884250189 59705817 634067024 32 | test 2935545032 1825735786 232756768 3755200068 33 | test 239780978 2959015142 1566667973 923468840 34 | test 1919120730 2264817815 1828022828 3087112924 35 | test 4284506471 2703581427 2135697121 1447697260 36 | test 1347766155 1321556739 2739367969 3654766471 37 | test 2550225148 2919958076 1000629778 3694769750 38 | test 3795641823 734041185 1038172538 361966593 39 | test 2607389877 1432584920 3449993480 159267285 40 | test 3755070732 3789984662 2380186768 2737185134 41 | test 1726991597 319676998 1390732362 2686830079 42 | test 77322880 744895757 1015688981 3673252259 43 | test 616992058 2616531630 2503711313 2624909199 44 | test 562713048 1492264745 2415288649 3991996287 45 | test 4243188571 4001759503 3240537900 2173172325 46 | test 416640787 526704750 3694486622 3407306691 47 | test 1831680456 651943736 2748155500 1705515647 48 | test 3903449888 3368710836 2214711510 661494373 49 | test 1385484 3682793950 185827478 81642545 50 | test 2285731259 1432092700 3880617508 1562555135 51 | test 4152947848 904414220 3214294466 74588203 52 | test 734682714 4164029086 2079100984 1191906118 53 | test 3308248605 3161127766 2244056456 2885822007 54 | test 4215925330 1658428724 199838792 321944299 55 | test 3148191512 2482996684 2071088089 3456212873 56 | test 258841653 863137642 771539396 67281421 57 | test 4055060879 2414764859 3565882918 1330077469 58 | test 441544498 473990981 2556231025 56725405 59 | test 238033503 2933147883 564449987 396856987 60 | test 1874598644 879725819 2231352531 3525520028 61 | test 3372337153 1429016478 1518858341 2621255367 62 | test 2862248269 4014267169 1357188235 2063330279 63 | test 1617803746 4270237678 972593458 2598181758 64 | test 3031480363 2695256774 3570659535 3824370192 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pure-random 2 | 3 | [![Build Status](https://travis-ci.org/Risto-Stevcev/pure-random.svg)](https://travis-ci.org/Risto-Stevcev/pure-random) 4 | 5 | A purely functional random number generator. It implements the `xorshift` algorithm. Xorshift RNGs which are a class of PRNGs that are extremely fast on modern computers and outperm other PRNGs in performance and BigCrush stress testing. 6 | 7 | 8 | ## Usage 9 | 10 | ```js 11 | > const rnd = require('pure-random') 12 | > rnd.genCsSeed() 13 | [ 2628481196, 1837393298, 2892949381, 1706851469 ] 14 | > rnd.random(rnd.genCsSeed(), 0, 10) 15 | Right(8) 16 | ``` 17 | 18 | Passing in the same seed with the same params produces the same number: 19 | 20 | ```js 21 | > const seed = rnd.genCsSeed() 22 | > seed 23 | [ 426141121, 700962946, 3633913687, 2605998810 ] 24 | > rnd.random(seed, 0, 10) 25 | Right(7) 26 | > rnd.random(seed, 0, 10) 27 | Right(7) 28 | > rnd.random(seed, 0, 10) 29 | Right(7) 30 | ``` 31 | 32 | 33 | ## Reference 34 | 35 | #### `genSeed` 36 | 37 | ```hs 38 | :: () -> [Uint32] 39 | ``` 40 | Uses `Date.now` to generate a non-cryptographically secure seed. It is more performant than `genCsSeed`, and appropriate for most cases. The seed is an array of four `Uint32` numbers. Since there is no `Uint32` in javascript yet, integers within the range **[0, 4294967295]** are returned. 41 | 42 | #### `genCsSeed` 43 | 44 | ```hs 45 | :: () -> [Uint32] 46 | ``` 47 | 48 | Uses `crypto.randomBytes` to generate a cryptographically secure seed. It is useful for creating an `initialization vector` for cryptography, or in any situation where you need the generated random number to have significant entropy. The seed is an array of four `Uint32` numbers. Since there is no `Uint32` in javascript yet, integers within the range **[0, 4294967295]** are returned. 49 | 50 | #### `randUint` 51 | 52 | ```hs 53 | :: [UInt32] -> UInt32 54 | ``` 55 | Takes a `seed` and returns a random `UInt32` value, which in javascript is an integer within the range **[0, 4294967295]**. This function is actually the javascript `xorshift` implementation, so it is extremely fast. The other methods call this method under the hood. 56 | 57 | #### `randFloat` 58 | 59 | ```hs 60 | :: [UInt32] -> Int -> Int -> Either Error Float 61 | ``` 62 | 63 | Takes a `seed` and a `min` and `max` range. It returns an `Either` value that is `Left Error` if the function was called with invalid parameters, or a `Right Float` within the specified range (inclusive). 64 | 65 | #### `random` 66 | 67 | ```hs 68 | :: [UInt32] -> Int -> Int -> Either Error Int 69 | ``` 70 | 71 | Takes a `seed` and a `min` and `max` range. It returns an `Either` value that is `Left Error` if the function was called with invalid parameters, or a `Right Int` within the specified range (inclusive). 72 | 73 | 74 | 75 | ## Faq 76 | 77 | #### Why wasn't Math.random used? 78 | 79 | `Math.random` is non-deterministic and doesn't have take a seed value, which means that you cannot return results consistently, which is against the purely functional paradigm. Note that this library has an option for more powerful entropy than `Math.random` by using the `genCsSeed` option. 80 | 81 | #### What about xorshift\* or xorshift+? 82 | 83 | The javascript number type uses a [double-precision 64-bit binary format IEEE 754 value](https://en.wikipedia.org/wiki/IEEE_floating_point), which is a number between **-(253 - 1)** and **253 - 1**. The xorshift\* and xorshift+ libraries rely on `Uint64` types, which javascript does not support. If a developer erroneously decides to represent `Uint64` using the native number type, then any `Uint64` type will lose precision on the upper end because a javascript number will only go up to **253 - 1** but a range of **0** to **264 - 1** is needed for `Uint64`. 84 | 85 | In general, it would be too complicated to implement these other algorithms because you would have to jump through a lot of hoops with javascript's lack of a type system, and with very little gain. These improvements to xorshift are only slightly more performant. 86 | -------------------------------------------------------------------------------- /test/pure-random.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const expect = require('chai').expect 3 | const jsc = require('jsverify') 4 | const rnd = require('../src/pure-random') 5 | const R = require('ramda') 6 | , S = require('sanctuary') 7 | 8 | const UINT32_MIN = 0 9 | const UINT32_MAX = 4294967295 10 | const inRange = R.both(R.lte(UINT32_MIN), R.gte(UINT32_MAX)) 11 | 12 | const seed = [ 4094795843, 75119171, 1064088777, 3185678737 ] 13 | , seed2 = [ 3085274235, 2506035445, 4237491691, 3384326347 ] 14 | 15 | describe('randUint', function() { 16 | it('should return the same result given the same seed', function() { 17 | for (let i = 0; i < 20; i++) 18 | expect(rnd.randUint(seed)).to.equal(3297453526) 19 | }) 20 | 21 | it('should return a different result for a different seed', function() { 22 | expect(rnd.randUint(seed2)).to.equal(1386574803) 23 | }) 24 | }) 25 | 26 | describe('rand', function() { 27 | it('should return a floating point number between [0, 1]', function() { 28 | expect(rnd.randFloat(seed, 0, 1)).to.deep.equal(S.Right(0.7677482270560573)) 29 | expect(rnd.randFloat(seed2, 0, 1)).to.deep.equal(S.Right(0.32283710393189385)) 30 | }) 31 | }) 32 | 33 | describe('random', function() { 34 | it('should return a Left value on an invalid seed (1)', function() { 35 | const result = rnd.random([0, 123, -1, 2], 1, 21) 36 | expect(result.isLeft).to.be.true 37 | expect(result.value).to.be.instanceof(Error) 38 | expect(result.value.message) 39 | .to.equal(`Seed must be an array of four integers between [${UINT32_MIN}, ${UINT32_MAX}]`) 40 | }) 41 | 42 | it('should return a Left value on an invalid seed (2)', function() { 43 | const result = rnd.random([12, UINT32_MAX + 1, 1, 2], 12, 21) 44 | expect(result.isLeft).to.be.true 45 | expect(result.value).to.be.instanceof(Error) 46 | expect(result.value.message) 47 | .to.equal(`Seed must be an array of four integers between [${UINT32_MIN}, ${UINT32_MAX}]`) 48 | }) 49 | 50 | it('should return a Left value on an invalid seed (3)', function() { 51 | const result = rnd.random([0, '123', 1, 2], 1, 21) 52 | expect(result.isLeft).to.be.true 53 | expect(result.value).to.be.instanceof(Error) 54 | expect(result.value.message) 55 | .to.equal(`Seed must be an array of four integers between [${UINT32_MIN}, ${UINT32_MAX}]`) 56 | }) 57 | 58 | it('should return a Left value if min is not a number', function() { 59 | const result = rnd.random(seed, '1', 21) 60 | expect(result.isLeft).to.be.true 61 | expect(result.value).to.be.instanceof(Error) 62 | expect(result.value.message).to.equal('Min must be a number') 63 | }) 64 | 65 | it('should return a Left value if max is not a number', function() { 66 | const result = rnd.random(seed, 1, '21') 67 | expect(result.isLeft).to.be.true 68 | expect(result.value).to.be.instanceof(Error) 69 | expect(result.value.message).to.equal('Max must be a number') 70 | }) 71 | 72 | it('should return a Left value if min >= max', function() { 73 | const result = rnd.random(seed, 1, 1) 74 | expect(result.isLeft).to.be.true 75 | expect(result.value).to.be.instanceof(Error) 76 | expect(result.value.message).to.equal('Min must be less than max') 77 | }) 78 | 79 | it('should return a Left value if min >= max', function() { 80 | const result = rnd.random(seed, 2, 1) 81 | expect(result.isLeft).to.be.true 82 | expect(result.value).to.be.instanceof(Error) 83 | expect(result.value.message).to.equal('Min must be less than max') 84 | }) 85 | }) 86 | 87 | 88 | describe('Stress tests', function() { 89 | this.timeout(5000) 90 | 91 | describe('random', function() { 92 | it('should get a random number in the valid range value (1)', function() { 93 | const validRange = jsc.forall(jsc.integer(0, 100), jsc.integer(101, 500), function(a, b) { 94 | const rand = rnd.random(seed, a, b) 95 | return rand.isRight && Number.isInteger(rand.value) && a <= rand.value && rand.value <= b 96 | }) 97 | jsc.assert(validRange, { tests: 10000 }) 98 | }) 99 | 100 | it('should get a random number in the valid range value (2)', function() { 101 | const validRange = jsc.forall(jsc.integer(-1000, 0), jsc.integer(1, 1000), function(a, b) { 102 | const rand = rnd.random(seed, a, b) 103 | return rand.isRight && Number.isInteger(rand.value) && a <= rand.value && rand.value <= b 104 | }) 105 | jsc.assert(validRange, { tests: 20000 }) 106 | }) 107 | }) 108 | 109 | describe('randFloat', function() { 110 | it('should get a rand number in the valid range value', function() { 111 | const validRange = jsc.forall(jsc.integer(-1000, 0), jsc.integer(1, 1000), function(a, b) { 112 | const rand = rnd.randFloat(seed, a, b) 113 | return rand.isRight && typeof rand.value === 'number' && !Number.isInteger(rand.value) && 114 | a <= rand.value && rand.value <= b 115 | }) 116 | jsc.assert(validRange, { tests: 20000 }) 117 | }) 118 | }) 119 | 120 | describe('randUint', function() { 121 | it('should get a rand number in the valid range value', function() { 122 | const validRange = jsc.forall('nat', function() { 123 | const rand = rnd.randUint(seed) 124 | return Number.isInteger(rand) && UINT32_MIN <= rand && rand <= UINT32_MAX 125 | }) 126 | jsc.assert(validRange, { tests: 20000 }) 127 | }) 128 | }) 129 | 130 | describe('genCsSeed', function() { 131 | it('should generate a seed array', function() { 132 | const validRange = jsc.forall('nat', function(a, b) { 133 | const seed = rnd.genCsSeed() 134 | return R.all(R.both(Number.isInteger, inRange), seed) 135 | }) 136 | jsc.assert(validRange, { tests: 10000 }) 137 | }) 138 | }) 139 | 140 | describe('genSeed', function() { 141 | it('should generate a seed array', function() { 142 | const validRange = jsc.forall('nat', function(a, b) { 143 | const seed = rnd.genSeed() 144 | return R.all(R.both(Number.isInteger, inRange), seed) 145 | }) 146 | jsc.assert(validRange, { tests: 10000 }) 147 | }) 148 | }) 149 | }) 150 | --------------------------------------------------------------------------------