Files
bambuddy/frontend/src/utils/random.ts
T

81 lines
2.7 KiB
TypeScript

const FNV1A_32_OFFSET_BASIS = 0x811c9dc5;
const FNV1A_32_PRIME = 0x01000193;
/**
* Computes a fast 32-bit FNV-1a hash for deterministic, non-security tasks.
* Accepts any number of string/nullable-string inputs, takes measurements
* to avoid collisions, and combines them into a 32-bit hash.
* Not cryptographically secure; use only for non-security-related use cases.
*/
export function hash_fnv1a32(...input: Array<string | null | undefined>): number {
let hash = FNV1A_32_OFFSET_BASIS;
const textEncoder = new TextEncoder();
const emptyElement = textEncoder.encode('__|');
for (const element of input) {
if (typeof element === 'string') {
hash = fnv1a32_update(hash, textEncoder.encode(element+'|'));
} else if (element === null || element === undefined) {
hash = fnv1a32_update(hash, emptyElement);
}
}
return hash >>> 0;
}
function fnv1a32_update(hash: number, value: Uint8Array): number {
for (const byte of value) {
hash ^= byte;
hash = Math.imul(hash, FNV1A_32_PRIME) >>> 0;
}
return hash;
}
export interface Mulberry32Sequence {
next(): number;
intBetween(from: number, to: number): number;
floatBetween(from: number, to: number): number;
}
/**
* Creates a fast deterministic PRNG sequence using Mulberry32.
* Same seed will always produce the same sequence.
* Not cryptographically secure; use only for non-security-related use cases.
*/
export function random_mulberry32(seed: number): Mulberry32Sequence {
const nextUint32 = (): number => {
seed |= 0;
seed = seed + 0x6D2B79F5 | 0;
let imul = Math.imul(seed ^ seed >>> 15, 1 | seed);
imul = imul + Math.imul(imul ^ imul >>> 7, 61 | imul) ^ imul;
return (imul ^ imul >>> 14) >>> 0;
};
const nextNormalized = (from: number, to: number): number => {
if (!Number.isFinite(from) || !Number.isFinite(to)) {
throw new RangeError('from and to must be finite numbers');
}
if (from > to) {
throw new RangeError('from must be less than or equal to to');
}
if (from === to) {
return from;
}
return from + nextUint32() / 0xFFFFFFFF * (to - from);
};
return {
next: () => {
return nextUint32() / 0xFFFFFFFF;
},
floatBetween: (from: number, to: number): number => {
return nextNormalized(from, to);
},
intBetween: (from: number, to: number): number => {
if (!Number.isInteger(from) || !Number.isInteger(to)) {
throw new RangeError('from and to must be integers');
}
return Math.round(nextNormalized(from, to));
},
};
}