From fd01d78d3d021edadd54c8c10f8c25a6d040a9a1 Mon Sep 17 00:00:00 2001 From: Mplan Date: Fri, 12 Jun 2026 18:57:10 +0800 Subject: [PATCH] feat: add version flag to CLI, enhance error handling for stdin and argument parsing --- README.md | 26 +++++++++++++------------- package.json | 3 ++- src/__tests__/args.test.ts | 8 ++++++++ src/args.ts | 18 ++++++------------ src/main.ts | 2 ++ src/stdin.ts | 2 +- 6 files changed, 32 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index a67b545..fe7dc66 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ rand -f2 # 保留 2 位小数 | `-f[N]` | 小数位数:`-f` 默认 2 位,`-f1` = 1 位,`-f3` = 3 位 | | `--float [N]` | `-f` 的长形式 | | `-d, --dist ` | 概率分布,默认 `uniform` | +| `-V, --version` | 版本信息 | | `-h, --help` | 帮助信息 | | `--` | 之后所有参数视为位置参数(用于负数) | @@ -45,14 +46,13 @@ rand -f2 # 保留 2 位小数 | poisson | `rand -d poisson` | `[λ]` | 1 | | exponential | `rand -d exponential` | `[λ]` | 1 | | hypergeometric | `rand -d hypergeometric` | `[N] [K] [n]` | 100, 50, 10 | - ### 采样算法 | 分布 | 算法 | |------|------| -| normal | Box-Muller 变换 | -| binomial | Bernoulli 试验求和 | -| poisson | Knuth 算法 | +| normal | Box-Muller 变换(双值缓存) | +| binomial | Bernoulli 试验求和;n > 10,000 时切换为正态近似 | +| poisson | Knuth 算法;λ > 100 时切换为正态近似(防止 exp 下溢) | | exponential | 逆 CDF 变换 | | hypergeometric | 不放回抽样模拟 | @@ -87,20 +87,20 @@ rand -d hypergeometric 100 30 5 # 负数范围(用 -- 分隔标志和参数) rand -- -10 -5 -rand -d normal -- 0 -1 # stddev 必须 > 0,会报错 -``` +rand -d normal 0 -1 # stddev 必须 > 0,会报错 ## 项目结构 ``` -index.ts # 入口 +index.ts # 入口 src/ - types.ts # 类型定义与默认值 - help.ts # 帮助文本 - args.ts # 参数解析与校验 - stdin.ts # 管道输入读取 - dist.ts # 分布采样器与调度 - main.ts # 编排逻辑 + types.ts # 类型定义与默认值 + help.ts # 帮助文本 + args.ts # 参数解析与校验 + stdin.ts # 管道输入读取 + dist.ts # 分布采样器与调度 + main.ts # 编排逻辑 + __tests__/ # 单元测试 (bun test) ``` ## 构建 diff --git a/package.json b/package.json index 5c2b6cf..96fc7a7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "rand": "./index.ts" }, "scripts": { - "build": "bun build ./index.ts --compile --outfile dist/rand" + "build": "bun build ./index.ts --compile --outfile dist/rand", + "test": "bun test" }, "private": true, "devDependencies": { diff --git a/src/__tests__/args.test.ts b/src/__tests__/args.test.ts index bc862a8..4c270d9 100644 --- a/src/__tests__/args.test.ts +++ b/src/__tests__/args.test.ts @@ -35,6 +35,14 @@ describe("parseArgs", () => { expect(() => parseArgs(["-c", "1000001"])).toThrow(); }); + test("-c with non-finite value throws", () => { + expect(() => parseArgs(["-c", "Infinity"])).toThrow("invalid count: Infinity"); + }); + + test("-f with out-of-range fails validation", () => { + expect(() => parseArgs(["-f", "101"])).toThrow("decimals must be 0–100"); + }); + test("-f with no arg defaults to 2", () => { expect(parseArgs(["-f"]).options.decimals).toBe(2); }); diff --git a/src/args.ts b/src/args.ts index 8ffdac8..dcb622b 100644 --- a/src/args.ts +++ b/src/args.ts @@ -122,61 +122,55 @@ export function applyPositionals(opts: Options, args: string[]): void { 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 { + } else if (nums.length > 2) { showError(`uniform expects 0–2 args, got ${nums.length}: ${args.join(" ")}`); } 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 { + } else if (nums.length > 2) { showError(`normal expects 0–2 args, got ${nums.length}: ${args.join(" ")}`); } 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 { + } else if (nums.length > 2) { showError(`binomial expects 0–2 args, got ${nums.length}: ${args.join(" ")}`); } break; } case "poisson": { - if (nums.length === 0) return; if (nums.length === 1) { opts.lambda = nums[0]!; - } else { + } else if (nums.length > 1) { showError(`poisson expects 0–1 arg, got ${nums.length}: ${args.join(" ")}`); } break; } case "exponential": { - if (nums.length === 0) return; if (nums.length === 1) { opts.lambda = nums[0]!; - } else { + } else if (nums.length > 1) { showError(`exponential expects 0–1 arg, got ${nums.length}: ${args.join(" ")}`); } 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]!; @@ -196,6 +190,6 @@ export function applyPositionals(opts: Options, args: string[]): void { function parseNum(s: string): number { const n = Number(s); - if (isNaN(n)) showError(`not a number: ${s}`); + if (isNaN(n) || !isFinite(n)) showError(`not a number: ${s}`); return n; } diff --git a/src/main.ts b/src/main.ts index 326d12f..78a1033 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,6 +15,8 @@ async function main(): Promise { const nums = parseStdinNumbers(input); if (nums.length > 0) { applyPositionals(options, nums.map(String)); + } else if (input.trim()) { + console.error("rand: stdin contained non-numeric data, ignoring"); } } diff --git a/src/stdin.ts b/src/stdin.ts index 59d54c0..96d7495 100644 --- a/src/stdin.ts +++ b/src/stdin.ts @@ -16,7 +16,7 @@ export function parseStdinNumbers(input: string): number[] { const nums: number[] = []; for (const p of parts) { const n = Number(p); - if (isNaN(n)) return []; // unrecognizable → ignore stdin + if (isNaN(n) || !isFinite(n)) return []; // unrecognizable → ignore stdin nums.push(n); } return nums;