203 lines
5.6 KiB
TypeScript
203 lines
5.6 KiB
TypeScript
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;
|
||
}
|