Files
rand-cli/src/args.ts
T

203 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}