repeatable randomization for JavaScript
JavaScript
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
source
test
.gitignore
LICENSE
README.md
eslintrc.json
index.js
package.json
rollup.config.js

README.md

rerandom.js

repeatable randomization for JavaScript

Overview

This is a randomization function which keeps track of the situations in which it is used. It will return a random value the first time it sees an input, and will then remember it and return the same output value for any subsequent uses of that input. Random first, then consistent.

How

ECMAScript 6 finally provides proper Maps so we no longer have to use plain objects to track key/value pairs. With Maps, keys don't necessarily have to be strings -- they can now be functions, objects, arrays, or whatever else you want. rerandom.js uses a Map to keep track of your inputs, which roughly represent the contexts in which you are using randomization, and it associates them with the results of the initial randomization. Essentially, it bundles random numbers, key/value tracking, and a few useful helper functions into a single tool so you can more easily use controlled, constrained randomization.

(Naturally, this requires ES6 Maps. If you don't have them in your environment, try one of the many available polyfills.)

Installation

Install via npm:

npm install rerandom

Or just include as a script tag:

<script type="text/javascript" src="path/to/rerandom.js"></script>

API

rerandom(factory);

Factory which returns an instance of the rerandom function. Each instance tracks previous values independently.

var rr = rerandom();

instance(input);

The main instance function returns a random float value between 0 and 1, just like Math.random(). It takes an optional input, and will return consistent output values whenever an input value is repeated.

The decision to reuse an existing previously randomized output value is based on JavaScript memory addressing. Anything that would pass an equality test will produce a consistent output value.

rr(4) === rr(4); // true
rr([]) === rr([]); // false
var a = [];
var b = [];
var c = a;
rr(a) === rr(b); // false
rr(a) === rr(c); // true

instance.registry(reset);

Returns or resets the internal Map object used as a registry for tracking inputs. If the optional reset argument is null, then the registry will be overwritten, and you'll start over with a clean slate again.

// reset the internal registry
a = rr(5);
rr.registry(null);
b = rr(5);
a === b; // false

// get the internal registry
let previous = rr.registry()
previous.has(5) // true

instance.generator(function);

Gets or sets a custom random number generator. Math.random() is used by default, but you can also roll your own using the Web Crypto API or other tools.

// always return the same integer (note: this is
// not a very good random number generator)
rr.generator(function() {
  return 42;
});
rr(5) === 42; // true

// replace the float generated by Math.random
// with an integer generated by the Web Crypto API
rr.generator(function() {
  return window.crypto.getRandomValues(new Uint32Array(1))[0];
});

Custom random number generators passed into the instance must be functions which always return numbers, and you'll get an error if you try to use anything else. To convert the random number outputs into arbitrary alternate types which may be more contextually useful, use the .post() method.

instance.post(function);

Gets or sets a post-processing function to be run on the results of the randomization. The float values returned by standard randomization often aren't immediately useful, so this is a way to bundle your desired behavior with the instance.

// return a float between 3 and 4
rr.post(function(value) {
  return value + 3;
});
rr(5) > 3 && rr(6) < 4; // true

// cast to string
rr.post(function(value) {
  return value + '';
});
typeof rr(7) === 'string'; // true

Post-processing functions can take up to two arguments: the output value of the random number generator and the original input. This allows you to create randomized outputs which are aware of the inputs. For example, to select a random item from an input array, you could do the following:

rr.post(function(value, array) {
    let index = Math.floor(value * array.length);
    return array[index];
});

You can also omit the input key and avoid updating the internal registry, which is clean a way to use the post-processing wrapper around Math.random.

rr.post(function(value) {
  let max = 10;
  return Math.floor(value * max) + 1;
});
rr(); // now always returns a value between 1 and 10

instance.key(function)

Gets or sets a preprocessing function which is applied to inputs before they are used as Map keys. Equality testing in JavaScript relies on memory addressing, such that primitives match more readily than objects and other complex data structures, but that is not always the desired behavior. This method attaches a function which can be used to increase the number of matches by extracting a primitive from a data structure, coercing a data structure into a primitive, flattening an array into a string, and so on.

For example, to get array literal inputs to more readily match up with previous values:

// array literals do not match by default
rr(['a', 'b', 'c']) === rr(['a', 'b', 'c']); // false

// flatten input array to a string before using as the Map key
rr.key(function(array) {
  return array + '';
});
rr(['a', 'b', 'c']);
rr.registry().has('a,b,c'); // true

// array literals now match based on key preprocessing
rr(['a', 'b', 'c']) === rr(['a', 'b', 'c']); // true

Or alternatively, to use the sum of all integers in the input array as the Map key:

rr.key(function(array) {
  return array.reduce(function(a, b) {return a + b;});
});
rr([1, 2]);
rr.registry().has(3); // true

Key processing functions take one argument, which is the input value, and must return whatever is to be used as the Map key, which in many cases is likely to be a string.

Further

  • Consider experimenting with networking multiple instances, such that the output of one instance is both used directly and also then fed into another instance. This may produce interesting effects, especially if the .key() and .post() methods are adjusted to this use case.
  • Randomization is deterministic within the context of a running page or application, but the record is reset whenever the page is reloaded, so values will not be deterministic across different page loads. For the latter, you'll need a random number generator that can be seeded; David Bau's seedrandom.js is a good option.