feat: add version flag, fix --float parsing, and optimize distributions
This commit is contained in:
+28
-29
@@ -1,17 +1,17 @@
|
||||
import { type Options, type Dist, DISTS, defaultOptions } from "./types";
|
||||
import { showHelp } from "./help";
|
||||
import { validateOptions } from "./dist";
|
||||
|
||||
export function showError(msg: string): never {
|
||||
console.error(`rand: ${msg}\nTry 'rand --help' for usage.`);
|
||||
process.exit(1);
|
||||
throw new Error(`${msg}\nTry 'rand --help' for usage.`);
|
||||
}
|
||||
|
||||
export function parseArgs(raw: string[]): { options: Options; help: boolean } {
|
||||
export function parseArgs(raw: string[]): { options: Options; help: boolean; version: boolean } {
|
||||
const positional: string[] = [];
|
||||
let count = 1;
|
||||
let decimals = 0;
|
||||
let dist: Dist = "uniform";
|
||||
let help = false;
|
||||
let version = false;
|
||||
|
||||
let i = 0;
|
||||
while (i < raw.length) {
|
||||
@@ -23,10 +23,21 @@ export function parseArgs(raw: string[]): { options: Options; help: boolean } {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "-V" || arg === "--version") {
|
||||
version = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// -f (plain) → default 2 decimal places
|
||||
if (arg === "-f") {
|
||||
decimals = 2;
|
||||
i++;
|
||||
if (i < raw.length && /^\d+$/.test(raw[i]!)) {
|
||||
decimals = parseInt(raw[i]!, 10);
|
||||
i++;
|
||||
} else {
|
||||
decimals = 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -54,21 +65,21 @@ export function parseArgs(raw: string[]): { options: Options; help: boolean } {
|
||||
i++;
|
||||
if (i >= raw.length) showError("--dist requires a distribution name");
|
||||
const name = raw[i]!;
|
||||
if (!(DISTS as string[]).includes(name)) {
|
||||
if (!DISTS.includes(name as Dist)) {
|
||||
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;
|
||||
const c = parseInt(raw[i]!, 10);
|
||||
if (isNaN(c) || c < 1) showError(`invalid count: ${raw[i]}`);
|
||||
if (c > 1_000_000) showError(`count must be <= 1,000,000, got ${c}`);
|
||||
count = c;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
@@ -103,8 +114,7 @@ export function parseArgs(raw: string[]): { options: Options; help: boolean } {
|
||||
opts.count = count;
|
||||
opts.decimals = decimals;
|
||||
applyPositionals(opts, positional);
|
||||
|
||||
return { options: opts, help };
|
||||
return { options: opts, help, version };
|
||||
}
|
||||
|
||||
export function applyPositionals(opts: Options, args: string[]): void {
|
||||
@@ -121,7 +131,6 @@ export function applyPositionals(opts: Options, args: string[]): void {
|
||||
} 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": {
|
||||
@@ -134,7 +143,6 @@ export function applyPositionals(opts: Options, args: string[]): void {
|
||||
} 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": {
|
||||
@@ -147,10 +155,6 @@ export function applyPositionals(opts: Options, args: string[]): void {
|
||||
} 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": {
|
||||
@@ -160,7 +164,6 @@ export function applyPositionals(opts: Options, args: string[]): void {
|
||||
} 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": {
|
||||
@@ -170,7 +173,6 @@ export function applyPositionals(opts: Options, args: string[]): void {
|
||||
} 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": {
|
||||
@@ -181,18 +183,15 @@ export function applyPositionals(opts: Options, args: string[]): void {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
validateOptions(opts);
|
||||
} catch (e) {
|
||||
showError((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function parseNum(s: string): number {
|
||||
|
||||
+78
-21
@@ -4,16 +4,34 @@ import { type Options } from "./types";
|
||||
// Individual distribution samplers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Box-Muller transform. Each call consumes 2 uniform randoms. */
|
||||
// Cached second normal deviate from Box-Muller transform.
|
||||
let spareNormal: number | null = null;
|
||||
|
||||
/** Box-Muller with pair caching: uses half the RNG calls of naive Box-Muller. */
|
||||
function normalRandom(mean: number, stddev: number): number {
|
||||
if (spareNormal !== null) {
|
||||
const v = spareNormal;
|
||||
spareNormal = null;
|
||||
return mean + stddev * v;
|
||||
}
|
||||
let u1 = Math.random();
|
||||
while (u1 === 0) u1 = Math.random(); // avoid log(0)
|
||||
for (let attempt = 0; u1 === 0 && attempt < 100; attempt++) {
|
||||
u1 = Math.random();
|
||||
}
|
||||
if (u1 === 0) u1 = Number.EPSILON;
|
||||
const u2 = Math.random();
|
||||
return mean + stddev * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
||||
const r = Math.sqrt(-2 * Math.log(u1));
|
||||
spareNormal = r * Math.sin(2 * Math.PI * u2);
|
||||
return mean + stddev * r * Math.cos(2 * Math.PI * u2);
|
||||
}
|
||||
|
||||
/** Sum of Bernoulli trials. */
|
||||
/** Bernoulli trials; Normal approximation when n>10_000 and np, n(1-p) both >5. */
|
||||
function binomialRandom(n: number, p: number): number {
|
||||
if (n > 10_000 && n * p > 5 && n * (1 - p) > 5) {
|
||||
const mean = n * p;
|
||||
const stddev = Math.sqrt(n * p * (1 - p));
|
||||
return Math.max(0, Math.min(n, Math.round(normalRandom(mean, stddev))));
|
||||
}
|
||||
let s = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
if (Math.random() < p) s++;
|
||||
@@ -21,8 +39,11 @@ function binomialRandom(n: number, p: number): number {
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Knuth's algorithm. */
|
||||
/** Knuth's algorithm; Normal approximation for λ > 100 (avoids exp underflow). */
|
||||
function poissonRandom(lambda: number): number {
|
||||
if (lambda > 100) {
|
||||
return Math.max(0, Math.round(normalRandom(lambda, Math.sqrt(lambda))));
|
||||
}
|
||||
const L = Math.exp(-lambda);
|
||||
let k = 0;
|
||||
let p = 1;
|
||||
@@ -33,9 +54,9 @@ function poissonRandom(lambda: number): number {
|
||||
return k - 1;
|
||||
}
|
||||
|
||||
/** Inverse CDF. */
|
||||
/** Inverse CDF. Caller must ensure λ > 0. */
|
||||
function exponentialRandom(lambda: number): number {
|
||||
return -Math.log(Math.random()) / lambda;
|
||||
return -Math.log(Math.random() || Number.EPSILON) / lambda;
|
||||
}
|
||||
|
||||
/** Urn model — simulate drawing without replacement. */
|
||||
@@ -58,31 +79,67 @@ function hypergeometricRandom(N: number, K: number, n: number): number {
|
||||
// Dispatcher
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function generate(opts: Options): number[] {
|
||||
const results: number[] = [];
|
||||
for (let i = 0; i < opts.count; i++) {
|
||||
export function validateOptions(opts: Options): void {
|
||||
if (!Number.isInteger(opts.count) || opts.count < 1) {
|
||||
throw new Error(`invalid count: ${opts.count}`);
|
||||
}
|
||||
if (!Number.isInteger(opts.decimals) || opts.decimals < 0 || opts.decimals > 100) {
|
||||
throw new Error(`decimals must be 0–100, got ${opts.decimals}`);
|
||||
}
|
||||
switch (opts.dist) {
|
||||
case "uniform":
|
||||
results.push(Math.random() * (opts.max - opts.min) + opts.min);
|
||||
if (opts.min > opts.max) throw new Error(`min (${opts.min}) > max (${opts.max})`);
|
||||
break;
|
||||
case "normal":
|
||||
results.push(normalRandom(opts.mean, opts.stddev));
|
||||
if (opts.stddev <= 0) throw new Error(`stddev must be > 0, got ${opts.stddev}`);
|
||||
break;
|
||||
case "binomial":
|
||||
results.push(binomialRandom(opts.trials, opts.prob));
|
||||
if (opts.trials < 0 || !Number.isInteger(opts.trials))
|
||||
throw new Error(`trials must be a non-negative integer, got ${opts.trials}`);
|
||||
if (opts.prob < 0 || opts.prob > 1)
|
||||
throw new Error(`prob must be 0–1, got ${opts.prob}`);
|
||||
break;
|
||||
case "poisson":
|
||||
results.push(poissonRandom(opts.lambda));
|
||||
break;
|
||||
case "exponential":
|
||||
results.push(exponentialRandom(opts.lambda));
|
||||
if (opts.lambda <= 0) throw new Error(`lambda must be > 0, got ${opts.lambda}`);
|
||||
break;
|
||||
case "hypergeometric":
|
||||
results.push(
|
||||
hypergeometricRandom(opts.popSize, opts.successes, opts.draws),
|
||||
);
|
||||
if (opts.popSize < 0 || !Number.isInteger(opts.popSize))
|
||||
throw new Error(`population size N must be a non-negative integer, got ${opts.popSize}`);
|
||||
if (opts.successes < 0 || opts.successes > opts.popSize || !Number.isInteger(opts.successes))
|
||||
throw new Error(`successes K must be 0–N, got ${opts.successes} (N=${opts.popSize})`);
|
||||
if (opts.draws < 0 || opts.draws > opts.popSize || !Number.isInteger(opts.draws))
|
||||
throw new Error(`draws n must be 0–N, got ${opts.draws} (N=${opts.popSize})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function* generate(opts: Options): Generator<number> {
|
||||
validateOptions(opts);
|
||||
for (let i = 0; i < opts.count; i++) {
|
||||
let v: number;
|
||||
switch (opts.dist) {
|
||||
case "uniform":
|
||||
v = opts.decimals === 0
|
||||
? Math.floor(Math.random() * (opts.max - opts.min + 1)) + opts.min
|
||||
: Math.random() * (opts.max - opts.min) + opts.min;
|
||||
break;
|
||||
case "normal":
|
||||
v = normalRandom(opts.mean, opts.stddev);
|
||||
break;
|
||||
case "binomial":
|
||||
v = binomialRandom(opts.trials, opts.prob);
|
||||
break;
|
||||
case "poisson":
|
||||
v = poissonRandom(opts.lambda);
|
||||
break;
|
||||
case "exponential":
|
||||
v = exponentialRandom(opts.lambda);
|
||||
break;
|
||||
case "hypergeometric":
|
||||
v = hypergeometricRandom(opts.popSize, opts.successes, opts.draws);
|
||||
break;
|
||||
}
|
||||
yield opts.decimals > 0 ? v : Math.round(v);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -1,3 +1,5 @@
|
||||
import { VERSION } from "./types";
|
||||
|
||||
export const HELP = `rand — Generate random numbers
|
||||
|
||||
Usage:
|
||||
@@ -5,10 +7,11 @@ Usage:
|
||||
|
||||
Options:
|
||||
-c, --count <n> Number of random numbers to output (default: 1)
|
||||
-f[N] Decimal places: -f = -f2 (default: 2)
|
||||
-f[N] Decimal places: -f = -f2, -f3 = 3 places
|
||||
--float [N] Long form of -f
|
||||
-d, --dist <name> Distribution (default: uniform)
|
||||
-h, --help Show this help
|
||||
-V, --version Show version
|
||||
|
||||
Distributions and their positional args:
|
||||
uniform rand [min] [max] (default: 0 100)
|
||||
@@ -33,3 +36,8 @@ export function showHelp(): never {
|
||||
console.log(HELP);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
export function showVersion(): never {
|
||||
console.log(`rand v${VERSION}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
+9
-8
@@ -1,12 +1,13 @@
|
||||
import { parseArgs, applyPositionals } from "./args";
|
||||
import { showHelp } from "./help";
|
||||
import { showHelp, showVersion } from "./help";
|
||||
import { readStdin, parseStdinNumbers } from "./stdin";
|
||||
import { generate } from "./dist";
|
||||
|
||||
export async function main(): Promise<void> {
|
||||
async function main(): Promise<void> {
|
||||
const argv = Bun.argv.slice(2);
|
||||
const { options, help } = parseArgs(argv);
|
||||
const { options, help, version } = parseArgs(argv);
|
||||
if (help) showHelp();
|
||||
if (version) showVersion();
|
||||
|
||||
// If stdin has data, parse numbers and apply as positional overrides
|
||||
if (!process.stdin.isTTY) {
|
||||
@@ -17,13 +18,13 @@ export async function main(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const results = generate(options);
|
||||
for (const r of results) {
|
||||
console.log(options.decimals > 0 ? r.toFixed(options.decimals) : Math.floor(r));
|
||||
for (const r of generate(options)) {
|
||||
console.log(options.decimals > 0 ? r.toFixed(options.decimals) : r);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err: Error) => {
|
||||
console.error(`rand: ${err.message}`);
|
||||
main().catch((err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`rand: ${msg}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
export async function readStdin(): Promise<string> {
|
||||
const decoder = new TextDecoder();
|
||||
let buf = "";
|
||||
const parts: string[] = [];
|
||||
for await (const chunk of Bun.stdin.stream()) {
|
||||
buf += decoder.decode(chunk, { stream: true });
|
||||
parts.push(decoder.decode(chunk, { stream: true }));
|
||||
}
|
||||
buf += decoder.decode(); // flush
|
||||
return buf;
|
||||
parts.push(decoder.decode()); // flush
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
/** Parse space-separated numbers from stdin. Returns number[] (may be empty). */
|
||||
|
||||
+6
-10
@@ -1,19 +1,15 @@
|
||||
export type Dist =
|
||||
| "uniform"
|
||||
| "normal"
|
||||
| "binomial"
|
||||
| "poisson"
|
||||
| "exponential"
|
||||
| "hypergeometric";
|
||||
|
||||
export const DISTS: Dist[] = [
|
||||
export const DISTS = [
|
||||
"uniform",
|
||||
"normal",
|
||||
"binomial",
|
||||
"poisson",
|
||||
"exponential",
|
||||
"hypergeometric",
|
||||
];
|
||||
] as const;
|
||||
|
||||
export type Dist = (typeof DISTS)[number];
|
||||
|
||||
export const VERSION = "1.0.0";
|
||||
|
||||
export interface Options {
|
||||
dist: Dist;
|
||||
|
||||
Reference in New Issue
Block a user