/*!
* Module Random
*/
/**
* @namespace Random
*/
const MODULE_NAME = 'Random';
//###[ IMPORTS ]########################################################################################################
import {orDefault, assert, hasValue, isFunction} from './basic.js';
import {pad} from './strings.js';
import {toBaseX} from './conversion.js';
//###[ DATA ]###########################################################################################################
const
RANDOM_UUIDS_USED_SINCE_RELOAD = new Set(),
DEFAULT_USER_CODE_ALPHABET = 'ACDEFGHKLMNPQRSTUVWXYZ2345679'
;
//###[ EXPORTS ]########################################################################################################
/**
* @namespace Random:randomNumber
*/
/**
* Special form of Math.random, returning a value in a defined range,
* where floor and ceiling are included in the range.
*
* By default, this method return an integer, but by setting "float" to true and
* optionally providing a float precision you can also work with floating point numbers.
*
* @param {?Number} [floor=0] - the lower end of random range, can either be integer or float
* @param {?Number} [ceiling=10] - the upper end of random range, can either be integer or float
* @param {?Boolean} [float=false] - define if we are working with floating point numbers
* @param {?Number} [precision=2] - if we are working with floats, what precision are we working with, considering floor, ceiling and result?
* @throws error if ceiling is smaller than floor
* @returns {Number} random integer or float between floor and ceiling
*
* @memberof Random:randomNumber
* @alias randomNumber
* @example
* let randomInt = randomNumber(23, 42);
* let randomFloat = randomNumber(23.5, 42.123, true, 3);
*/
export function randomNumber(floor=0, ceiling=10, float=false, precision=2){
floor = orDefault(floor, 0, 'float');
ceiling = orDefault(ceiling, 10, 'float');
float = orDefault(float, false, 'bool');
precision = orDefault(precision, 2, 'int');
assert((ceiling >= floor), `${MODULE_NAME}:randomInt | ceiling smaller than floor`);
const power = Math.pow(10, precision);
if( float ){
floor *= power;
ceiling *= power;
}
const res = Math.floor(Math.random() * (ceiling - floor + 1) + floor);
return float ? ((Math.round(parseFloat(res) * power) / power) / power) : res;
}
/**
* @namespace Random:randomUuid
*/
/**
* Generate a RFC4122-compliant random UUID, as far as possible with JS.
* Generation is heavily dependent on the quality of randomization, which in some JS-engines is weak using
* Math.random. Therefore, we are using the specific crypto api if available and only fall back to random if necessary.
* Additionally, we track used UUIDs to never return the same id twice per reload.
*
* For a detailed discussion, see: https://stackoverflow.com/a/2117523
*
* @param {?Boolean} [withDashes=true] - defines if UUID shall include dashes or not
* @throws error if too many collisions happen and the random implementation seems to be broken
* @returns {String} a "UUID"
*
* @memberof Random:randomUuid
* @alias randomUuid
* @example
* const uuidWithDashes = randomUuid();
* const uuidWithoutDashes = randomUuid(false);
*/
export function randomUuid(withDashes=true){
withDashes = orDefault(withDashes, true, 'bool');
let
uuid = null,
collisions = 0
;
while( !hasValue(uuid) || RANDOM_UUIDS_USED_SINCE_RELOAD.has(uuid) ){
// we have to do this highly convoluted check, because we have to call getRandomValues
// explicitly from either window.crypto or window.msCrypto, since invoking it from another
// context will trigger an "illegal invocation" of the method :(
if(
isFunction(window.crypto?.getRandomValues)
|| isFunction(window.msCrypto?.getRandomValues)
){
uuid = ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (
c
^ (
isFunction(window.crypto?.getRandomValues)
? window.crypto.getRandomValues(new Uint8Array(1))
: window.msCrypto?.getRandomValues(new Uint8Array(1))
)[0]
& 15 >> c / 4
).toString(16));
} else {
uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const
r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8)
;
return v.toString(16);
});
}
if( RANDOM_UUIDS_USED_SINCE_RELOAD.has(uuid) ){
collisions++;
if( collisions > 100 ){
assert(collisions <= 100, `${MODULE_NAME}:randomUuid | too many collisions, there seems to be randomization problem`)
}
}
}
RANDOM_UUIDS_USED_SINCE_RELOAD.add(uuid);
return withDashes ? uuid : uuid.replace(/-/g, '');
}
/**
* @namespace Random:randomUserCode
*/
/**
* Generates a random code, to be presented to the user, being easily readable and concise.
* Use this for things like, coupon codes, session IDs and everything, that might be transcribed by hand.
*
* The algorithm used is using time-based information in combination with a larger random number, so, there should not
* be any collisions, but build in a fail-safe, if you persist this code to a database, to make absolutely sure, that
* the code is unique.
*
* The used method here is formulated, to result in a short, concise highly readable code, while keeping the value
* highly random and as collision-free as possible. The basis for this is a combination of a compressed ISO-datetime
* string and a crypto-random-based combination of several random Uint8-values.
*
* Hint: if you need a general implementation to convert a value to a certain alphabet/base, have a look at
* `Conversion:toBaseX`.
*
* @param {?String} [alphabet='ACDEFGHKLMNPQRSTUVWXYZ2345679'] - the character pool to use for code generation
* @param {?String} [paddingCharacter='8'] - the character to use for value padding if generated code is too short
* @param {?Number} [minLength=8] - the min length, the code has to have at least, will be padded if too short
* @param {?Number} [maxLength=12] - the max length, the code can have at most, a code longer than this, will result in an error
* @param {?Number} [randomValue=null] - random integer to include in the code's base value, should be ~6 digits, will automatically be generated if missing
* @throws error if maxLength is smaller than minLength
* @throws error if the generated code is longer than maxLength
* @returns {String} the generated user code
*
* @memberof Random:randomUserCode
* @alias randomUserCode
* @see Conversion:toBaseX
* @example
* randomUserCode()
* => 'GVK6RNQ8'
* randomUserCode('0123456789ABCDEF', 10, 10, '=')
* => 'A03CF25D7='
*/
export function randomUserCode(
alphabet=DEFAULT_USER_CODE_ALPHABET,
paddingCharacter='8',
minLength=8,
maxLength=12,
randomValue=null
){
const __methodName__ = 'randomUserCode';
alphabet = orDefault(alphabet, DEFAULT_USER_CODE_ALPHABET, 'str');
paddingCharacter = orDefault(paddingCharacter, '8', 'str')[0];
minLength = orDefault(minLength, 8, 'int');
maxLength = orDefault(maxLength, 12, 'int');
randomValue = orDefault(
randomValue,
window.crypto?.getRandomValues?.(new Uint16Array(15)).reduce((sum, v) => sum + v, 0)
?? window.msCrypto?.getRandomValues?.(new Uint16Array(15)).reduce((sum, v) => sum + v, 0)
?? randomNumber(1, 999999)
,
'int'
);
if( maxLength < minLength ){
throw Error(`${MODULE_NAME}:${__methodName__} | minLength cannot be smaller than maxLength`);
}
let code = ''+toBaseX(Number(
`${randomValue}${
(new Date()).toISOString().replace(/[\-T:.Z]/g, '?')
.split('?')
.reduce((sum, v) => sum + Number(v), 0)
}`
), alphabet);
if( code.length > maxLength ){
throw Error(
`${MODULE_NAME}:${__methodName__} | code too long, check maxLength and custom randomValue`
);
} else if( code.length < minLength ){
code = pad(code, paddingCharacter, minLength, 'right');
}
return code;
}