Release Random Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
import { bytesToHex } from "@add-ideas/toolbox-helpers";
|
||||
import type { RandomSource } from "./source";
|
||||
|
||||
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
||||
export const DEFAULT_WORDS = [
|
||||
"amber",
|
||||
"anchor",
|
||||
"apple",
|
||||
"april",
|
||||
"arch",
|
||||
"arrow",
|
||||
"atlas",
|
||||
"autumn",
|
||||
"bamboo",
|
||||
"beacon",
|
||||
"berry",
|
||||
"birch",
|
||||
"blue",
|
||||
"breeze",
|
||||
"brook",
|
||||
"cabin",
|
||||
"candle",
|
||||
"cedar",
|
||||
"cherry",
|
||||
"cloud",
|
||||
"clover",
|
||||
"cobalt",
|
||||
"comet",
|
||||
"coral",
|
||||
"crane",
|
||||
"crystal",
|
||||
"dawn",
|
||||
"delta",
|
||||
"dune",
|
||||
"eagle",
|
||||
"earth",
|
||||
"ember",
|
||||
"fern",
|
||||
"field",
|
||||
"finch",
|
||||
"flame",
|
||||
"forest",
|
||||
"frost",
|
||||
"garden",
|
||||
"glade",
|
||||
"gold",
|
||||
"granite",
|
||||
"harbor",
|
||||
"hazel",
|
||||
"hill",
|
||||
"honey",
|
||||
"indigo",
|
||||
"iris",
|
||||
"island",
|
||||
"ivory",
|
||||
"jade",
|
||||
"jasmine",
|
||||
"juniper",
|
||||
"lake",
|
||||
"lantern",
|
||||
"leaf",
|
||||
"lemon",
|
||||
"lilac",
|
||||
"lotus",
|
||||
"luna",
|
||||
"maple",
|
||||
"marble",
|
||||
"meadow",
|
||||
"mint",
|
||||
"mist",
|
||||
"moon",
|
||||
"moss",
|
||||
"oasis",
|
||||
"ocean",
|
||||
"olive",
|
||||
"opal",
|
||||
"orchid",
|
||||
"peach",
|
||||
"pearl",
|
||||
"pine",
|
||||
"plum",
|
||||
"pond",
|
||||
"poppy",
|
||||
"quartz",
|
||||
"rain",
|
||||
"raven",
|
||||
"reef",
|
||||
"river",
|
||||
"rose",
|
||||
"ruby",
|
||||
"sage",
|
||||
"sand",
|
||||
"sea",
|
||||
"shadow",
|
||||
"shell",
|
||||
"silver",
|
||||
"sky",
|
||||
"snow",
|
||||
"solar",
|
||||
"sparrow",
|
||||
"spring",
|
||||
"star",
|
||||
"stone",
|
||||
"storm",
|
||||
"sun",
|
||||
"teal",
|
||||
"thistle",
|
||||
"tide",
|
||||
"timber",
|
||||
"topaz",
|
||||
"trail",
|
||||
"tree",
|
||||
"tulip",
|
||||
"valley",
|
||||
"violet",
|
||||
"wave",
|
||||
"willow",
|
||||
"wind",
|
||||
"winter",
|
||||
"wood",
|
||||
"wren",
|
||||
"zephyr",
|
||||
] as const;
|
||||
|
||||
export function randomIntegers(
|
||||
source: RandomSource,
|
||||
count: number,
|
||||
minimum: number,
|
||||
maximumInclusive: number,
|
||||
): number[] {
|
||||
if (!Number.isSafeInteger(count) || count < 1 || count > 100_000)
|
||||
throw new Error("Count must be 1–100,000.");
|
||||
if (
|
||||
!Number.isSafeInteger(minimum) ||
|
||||
!Number.isSafeInteger(maximumInclusive) ||
|
||||
maximumInclusive < minimum ||
|
||||
maximumInclusive - minimum >= 2 ** 32
|
||||
)
|
||||
throw new Error(
|
||||
"Integer bounds must be safe, ordered, and span fewer than 2³² values.",
|
||||
);
|
||||
return Array.from({ length: count }, () =>
|
||||
source.integer(minimum, maximumInclusive + 1),
|
||||
);
|
||||
}
|
||||
|
||||
export function randomString(
|
||||
source: RandomSource,
|
||||
length: number,
|
||||
alphabet: string,
|
||||
): string {
|
||||
if (alphabet.length > 131_072)
|
||||
throw new Error("Alphabet exceeds the 131,072 UTF-16-unit input limit.");
|
||||
const symbols = [...alphabet];
|
||||
if (!Number.isSafeInteger(length) || length < 1 || length > 1_000_000)
|
||||
throw new Error("Length must be 1–1,000,000 code points.");
|
||||
if (symbols.length < 2 || symbols.length > 65_536)
|
||||
throw new Error("Alphabet must contain 2–65,536 code points.");
|
||||
if (new Set(symbols).size !== symbols.length)
|
||||
throw new Error(
|
||||
"Alphabet contains duplicate code points, which would bias output.",
|
||||
);
|
||||
return Array.from(
|
||||
{ length },
|
||||
() => symbols[source.integer(0, symbols.length)]!,
|
||||
).join("");
|
||||
}
|
||||
|
||||
export function uuidV4(source: RandomSource): string {
|
||||
const bytes = source.bytes(16);
|
||||
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
||||
return formatUuid(bytes);
|
||||
}
|
||||
|
||||
export function uuidV7(source: RandomSource, now = Date.now()): string {
|
||||
if (!Number.isSafeInteger(now) || now < 0 || now >= 2 ** 48)
|
||||
throw new Error("UUIDv7 timestamp is outside the 48-bit range.");
|
||||
const bytes = source.bytes(16);
|
||||
let timestamp = BigInt(now);
|
||||
for (let index = 5; index >= 0; index -= 1) {
|
||||
bytes[index] = Number(timestamp & 0xffn);
|
||||
timestamp >>= 8n;
|
||||
}
|
||||
bytes[6] = (bytes[6]! & 0x0f) | 0x70;
|
||||
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
||||
return formatUuid(bytes);
|
||||
}
|
||||
|
||||
function formatUuid(bytes: Uint8Array): string {
|
||||
const value = bytesToHex(bytes);
|
||||
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
||||
}
|
||||
|
||||
export function ulid(source: RandomSource, now = Date.now()): string {
|
||||
if (!Number.isSafeInteger(now) || now < 0 || now >= 2 ** 48)
|
||||
throw new Error("ULID timestamp is outside the 48-bit range.");
|
||||
let time = BigInt(now);
|
||||
let left = "";
|
||||
for (let index = 0; index < 10; index += 1) {
|
||||
left = CROCKFORD[Number(time & 31n)]! + left;
|
||||
time >>= 5n;
|
||||
}
|
||||
let random = 0n;
|
||||
for (const byte of source.bytes(10)) random = (random << 8n) | BigInt(byte);
|
||||
let right = "";
|
||||
for (let index = 0; index < 16; index += 1) {
|
||||
right = CROCKFORD[Number(random & 31n)]! + right;
|
||||
random >>= 5n;
|
||||
}
|
||||
return left + right;
|
||||
}
|
||||
|
||||
export interface DiceResult {
|
||||
expression: string;
|
||||
rolls: number[];
|
||||
modifier: number;
|
||||
total: number;
|
||||
}
|
||||
export function rollDice(source: RandomSource, expression: string): DiceResult {
|
||||
const match =
|
||||
/^\s*(\d{1,6})d(\d{1,10})(?:\s*([+-])\s*(\d{1,12}))?\s*$/iu.exec(
|
||||
expression,
|
||||
);
|
||||
if (!match) throw new Error("Use dice notation such as 4d6+2.");
|
||||
const count = Number(match[1]);
|
||||
const sides = Number(match[2]);
|
||||
const modifier = match[3]
|
||||
? Number(match[4]) * (match[3] === "-" ? -1 : 1)
|
||||
: 0;
|
||||
if (
|
||||
count < 1 ||
|
||||
count > 100_000 ||
|
||||
sides < 2 ||
|
||||
sides > 1_000_000_000 ||
|
||||
!Number.isSafeInteger(modifier)
|
||||
)
|
||||
throw new Error(
|
||||
"Dice limits: 1–100,000 dice, 2–1,000,000,000 sides, safe-integer modifier.",
|
||||
);
|
||||
const rolls = Array.from({ length: count }, () =>
|
||||
source.integer(1, sides + 1),
|
||||
);
|
||||
return {
|
||||
expression: `${count}d${sides}${modifier ? (modifier > 0 ? `+${modifier}` : String(modifier)) : ""}`,
|
||||
rolls,
|
||||
modifier,
|
||||
total: rolls.reduce((sum, value) => sum + value, modifier),
|
||||
};
|
||||
}
|
||||
|
||||
export function passphrase(
|
||||
source: RandomSource,
|
||||
count: number,
|
||||
words: readonly string[] = DEFAULT_WORDS,
|
||||
separator = "-",
|
||||
): { value: string; entropy: number } {
|
||||
if (words.length > 100_000 || words.some((word) => word.length > 10_000))
|
||||
throw new Error(
|
||||
"Word lists are limited to 100,000 entries and 10,000 UTF-16 units per entry.",
|
||||
);
|
||||
const clean = words.map((word) => word.trim()).filter(Boolean);
|
||||
if (!Number.isSafeInteger(count) || count < 1 || count > 100)
|
||||
throw new Error("Passphrase word count must be 1–100.");
|
||||
if (new Set(clean).size < 2 || new Set(clean).size !== clean.length)
|
||||
throw new Error(
|
||||
"Word list must contain at least two unique non-empty entries.",
|
||||
);
|
||||
return {
|
||||
value: Array.from(
|
||||
{ length: count },
|
||||
() => clean[source.integer(0, clean.length)]!,
|
||||
).join(separator),
|
||||
entropy: count * Math.log2(clean.length),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalValues(
|
||||
source: RandomSource,
|
||||
count: number,
|
||||
mean: number,
|
||||
deviation: number,
|
||||
): number[] {
|
||||
if (
|
||||
!Number.isSafeInteger(count) ||
|
||||
count < 1 ||
|
||||
count > 100_000 ||
|
||||
!Number.isFinite(mean) ||
|
||||
!Number.isFinite(deviation) ||
|
||||
deviation <= 0
|
||||
)
|
||||
throw new Error(
|
||||
"Use a count of 1–100,000 and a positive finite deviation.",
|
||||
);
|
||||
const values: number[] = [];
|
||||
while (values.length < count) {
|
||||
const u = Math.max(Number.EPSILON, source.float());
|
||||
const v = source.float();
|
||||
const radius = Math.sqrt(-2 * Math.log(u));
|
||||
const first = mean + deviation * radius * Math.cos(2 * Math.PI * v);
|
||||
const second = mean + deviation * radius * Math.sin(2 * Math.PI * v);
|
||||
if (!Number.isFinite(first) || !Number.isFinite(second))
|
||||
throw new Error(
|
||||
"Normal-distribution parameters produced a non-finite result.",
|
||||
);
|
||||
values.push(first);
|
||||
if (values.length < count) values.push(second);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export function sampleValues<T>(
|
||||
source: RandomSource,
|
||||
values: readonly T[],
|
||||
count: number,
|
||||
): T[] {
|
||||
if (!Number.isSafeInteger(count) || count < 1 || count > values.length)
|
||||
throw new Error(
|
||||
"Sample count must be between 1 and the number of input items.",
|
||||
);
|
||||
return source.shuffle(values).slice(0, count);
|
||||
}
|
||||
Reference in New Issue
Block a user