Release Random Tools 0.1.0

This commit is contained in:
2026-09-01 02:53:47 +02:00
commit 2ed2560b95
59 changed files with 8692 additions and 0 deletions
+322
View File
@@ -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 1100,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 11,000,000 code points.");
if (symbols.length < 2 || symbols.length > 65_536)
throw new Error("Alphabet must contain 265,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: 1100,000 dice, 21,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 1100.");
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 1100,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);
}