feat: add CLI random number generator supporting 6 distributions

This commit is contained in:
2026-06-12 14:14:31 +08:00
commit 2a17ca30cb
11 changed files with 634 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
import { type Options, type Dist, DISTS, defaultOptions } from "./types";
import { showHelp } from "./help";
export function showError(msg: string): never {
console.error(`rand: ${msg}\nTry 'rand --help' for usage.`);
process.exit(1);
}
export function parseArgs(raw: string[]): { options: Options; help: boolean } {
const positional: string[] = [];
let count = 1;
let decimals = 0;
let dist: Dist = "uniform";
let help = false;
let i = 0;
while (i < raw.length) {
const arg = raw[i]!;
if (arg === "-h" || arg === "--help") {
help = true;
i++;
continue;
}
// -f (plain) → default 2 decimal places
if (arg === "-f") {
decimals = 2;
i++;
continue;
}
// -fN combined form: -f1, -f2, -f3, …
if (/^-f\d+$/.test(arg)) {
decimals = parseInt(arg.slice(2), 10);
i++;
continue;
}
// --float [N]
if (arg === "--float") {
i++;
if (i < raw.length && /^\d+$/.test(raw[i]!)) {
decimals = parseInt(raw[i]!, 10);
i++;
} else {
decimals = 2;
}
continue;
}
// -d, --dist <name>
if (arg === "-d" || arg === "--dist") {
i++;
if (i >= raw.length) showError("--dist requires a distribution name");
const name = raw[i]!;
if (!(DISTS as string[]).includes(name)) {
showError(`unknown distribution: ${name}\n valid: ${DISTS.join(", ")}`);
}
dist = name as Dist;
i++;
continue;
}
// -c, --count <n>
if (arg === "-c" || arg === "--count") {
i++;
if (i >= raw.length) showError("--count requires a value");
const n = parseInt(raw[i]!, 10);
if (isNaN(n) || n < 1) showError(`invalid count: ${raw[i]}`);
count = n;
i++;
continue;
}
// -- : everything after is positional
if (arg === "--") {
i++;
while (i < raw.length) {
positional.push(raw[i]!);
i++;
}
break;
}
// Negative number as positional (e.g. -1, -0.5)
if (/^-\d+(\.\d+)?$/.test(arg)) {
positional.push(arg);
i++;
continue;
}
// Catch unknown flags
if (arg.startsWith("-")) {
showError(`unknown option: ${arg}`);
}
positional.push(arg);
i++;
}
const opts = defaultOptions(dist);
opts.count = count;
opts.decimals = decimals;
applyPositionals(opts, positional);
return { options: opts, help };
}
export function applyPositionals(opts: Options, args: string[]): void {
const nums = args.map(parseNum);
switch (opts.dist) {
case "uniform": {
if (nums.length === 0) return;
if (nums.length === 1) {
opts.max = nums[0]!;
} else if (nums.length === 2) {
opts.min = nums[0]!;
opts.max = nums[1]!;
} else {
showError(`uniform expects 02 args, got ${nums.length}: ${args.join(" ")}`);
}
if (opts.min > opts.max) showError(`min (${opts.min}) > max (${opts.max})`);
break;
}
case "normal": {
if (nums.length === 0) return;
if (nums.length === 1) {
opts.mean = nums[0]!;
} else if (nums.length === 2) {
opts.mean = nums[0]!;
opts.stddev = nums[1]!;
} else {
showError(`normal expects 02 args, got ${nums.length}: ${args.join(" ")}`);
}
if (opts.stddev <= 0) showError(`stddev must be > 0, got ${opts.stddev}`);
break;
}
case "binomial": {
if (nums.length === 0) return;
if (nums.length === 1) {
opts.trials = nums[0]!;
} else if (nums.length === 2) {
opts.trials = nums[0]!;
opts.prob = nums[1]!;
} else {
showError(`binomial expects 02 args, got ${nums.length}: ${args.join(" ")}`);
}
if (opts.trials < 0 || !Number.isInteger(opts.trials)) {
showError(`trials must be a non-negative integer, got ${opts.trials}`);
}
if (opts.prob < 0 || opts.prob > 1) showError(`prob must be 01, got ${opts.prob}`);
break;
}
case "poisson": {
if (nums.length === 0) return;
if (nums.length === 1) {
opts.lambda = nums[0]!;
} else {
showError(`poisson expects 01 arg, got ${nums.length}: ${args.join(" ")}`);
}
if (opts.lambda <= 0) showError(`lambda must be > 0, got ${opts.lambda}`);
break;
}
case "exponential": {
if (nums.length === 0) return;
if (nums.length === 1) {
opts.lambda = nums[0]!;
} else {
showError(`exponential expects 01 arg, got ${nums.length}: ${args.join(" ")}`);
}
if (opts.lambda <= 0) showError(`lambda must be > 0, got ${opts.lambda}`);
break;
}
case "hypergeometric": {
if (nums.length === 0) return;
if (nums.length >= 1) opts.popSize = nums[0]!;
if (nums.length >= 2) opts.successes = nums[1]!;
if (nums.length >= 3) opts.draws = nums[2]!;
if (nums.length > 3) {
showError(`hypergeometric expects 03 args, got ${nums.length}: ${args.join(" ")}`);
}
if (opts.popSize < 0 || !Number.isInteger(opts.popSize)) {
showError(`population size N must be a non-negative integer, got ${opts.popSize}`);
}
if (opts.successes < 0 || opts.successes > opts.popSize || !Number.isInteger(opts.successes)) {
showError(`successes K must be 0N, got ${opts.successes} (N=${opts.popSize})`);
}
if (opts.draws < 0 || opts.draws > opts.popSize || !Number.isInteger(opts.draws)) {
showError(`draws n must be 0N, got ${opts.draws} (N=${opts.popSize})`);
}
break;
}
}
}
function parseNum(s: string): number {
const n = Number(s);
if (isNaN(n)) showError(`not a number: ${s}`);
return n;
}