feat: add CLI random number generator supporting 6 distributions
This commit is contained in:
+202
@@ -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 0–2 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 0–2 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 0–2 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 0–1, got ${opts.prob}`);
|
||||
break;
|
||||
}
|
||||
case "poisson": {
|
||||
if (nums.length === 0) return;
|
||||
if (nums.length === 1) {
|
||||
opts.lambda = nums[0]!;
|
||||
} else {
|
||||
showError(`poisson expects 0–1 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 0–1 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 0–3 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 0–N, got ${opts.successes} (N=${opts.popSize})`);
|
||||
}
|
||||
if (opts.draws < 0 || opts.draws > opts.popSize || !Number.isInteger(opts.draws)) {
|
||||
showError(`draws n must be 0–N, 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;
|
||||
}
|
||||
Reference in New Issue
Block a user