feat: add version flag to CLI, enhance error handling for stdin and argument parsing
This commit is contained in:
@@ -32,6 +32,7 @@ rand -f2 # 保留 2 位小数
|
|||||||
| `-f[N]` | 小数位数:`-f` 默认 2 位,`-f1` = 1 位,`-f3` = 3 位 |
|
| `-f[N]` | 小数位数:`-f` 默认 2 位,`-f1` = 1 位,`-f3` = 3 位 |
|
||||||
| `--float [N]` | `-f` 的长形式 |
|
| `--float [N]` | `-f` 的长形式 |
|
||||||
| `-d, --dist <name>` | 概率分布,默认 `uniform` |
|
| `-d, --dist <name>` | 概率分布,默认 `uniform` |
|
||||||
|
| `-V, --version` | 版本信息 |
|
||||||
| `-h, --help` | 帮助信息 |
|
| `-h, --help` | 帮助信息 |
|
||||||
| `--` | 之后所有参数视为位置参数(用于负数) |
|
| `--` | 之后所有参数视为位置参数(用于负数) |
|
||||||
|
|
||||||
@@ -45,14 +46,13 @@ rand -f2 # 保留 2 位小数
|
|||||||
| poisson | `rand -d poisson` | `[λ]` | 1 |
|
| poisson | `rand -d poisson` | `[λ]` | 1 |
|
||||||
| exponential | `rand -d exponential` | `[λ]` | 1 |
|
| exponential | `rand -d exponential` | `[λ]` | 1 |
|
||||||
| hypergeometric | `rand -d hypergeometric` | `[N] [K] [n]` | 100, 50, 10 |
|
| hypergeometric | `rand -d hypergeometric` | `[N] [K] [n]` | 100, 50, 10 |
|
||||||
|
|
||||||
### 采样算法
|
### 采样算法
|
||||||
|
|
||||||
| 分布 | 算法 |
|
| 分布 | 算法 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| normal | Box-Muller 变换 |
|
| normal | Box-Muller 变换(双值缓存) |
|
||||||
| binomial | Bernoulli 试验求和 |
|
| binomial | Bernoulli 试验求和;n > 10,000 时切换为正态近似 |
|
||||||
| poisson | Knuth 算法 |
|
| poisson | Knuth 算法;λ > 100 时切换为正态近似(防止 exp 下溢) |
|
||||||
| exponential | 逆 CDF 变换 |
|
| exponential | 逆 CDF 变换 |
|
||||||
| hypergeometric | 不放回抽样模拟 |
|
| hypergeometric | 不放回抽样模拟 |
|
||||||
|
|
||||||
@@ -87,20 +87,20 @@ rand -d hypergeometric 100 30 5
|
|||||||
|
|
||||||
# 负数范围(用 -- 分隔标志和参数)
|
# 负数范围(用 -- 分隔标志和参数)
|
||||||
rand -- -10 -5
|
rand -- -10 -5
|
||||||
rand -d normal -- 0 -1 # stddev 必须 > 0,会报错
|
rand -d normal 0 -1 # stddev 必须 > 0,会报错
|
||||||
```
|
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
index.ts # 入口
|
index.ts # 入口
|
||||||
src/
|
src/
|
||||||
types.ts # 类型定义与默认值
|
types.ts # 类型定义与默认值
|
||||||
help.ts # 帮助文本
|
help.ts # 帮助文本
|
||||||
args.ts # 参数解析与校验
|
args.ts # 参数解析与校验
|
||||||
stdin.ts # 管道输入读取
|
stdin.ts # 管道输入读取
|
||||||
dist.ts # 分布采样器与调度
|
dist.ts # 分布采样器与调度
|
||||||
main.ts # 编排逻辑
|
main.ts # 编排逻辑
|
||||||
|
__tests__/ # 单元测试 (bun test)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
"rand": "./index.ts"
|
"rand": "./index.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun build ./index.ts --compile --outfile dist/rand"
|
"build": "bun build ./index.ts --compile --outfile dist/rand",
|
||||||
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ describe("parseArgs", () => {
|
|||||||
expect(() => parseArgs(["-c", "1000001"])).toThrow();
|
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", () => {
|
test("-f with no arg defaults to 2", () => {
|
||||||
expect(parseArgs(["-f"]).options.decimals).toBe(2);
|
expect(parseArgs(["-f"]).options.decimals).toBe(2);
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-12
@@ -122,61 +122,55 @@ export function applyPositionals(opts: Options, args: string[]): void {
|
|||||||
|
|
||||||
switch (opts.dist) {
|
switch (opts.dist) {
|
||||||
case "uniform": {
|
case "uniform": {
|
||||||
if (nums.length === 0) return;
|
|
||||||
if (nums.length === 1) {
|
if (nums.length === 1) {
|
||||||
opts.max = nums[0]!;
|
opts.max = nums[0]!;
|
||||||
} else if (nums.length === 2) {
|
} else if (nums.length === 2) {
|
||||||
opts.min = nums[0]!;
|
opts.min = nums[0]!;
|
||||||
opts.max = nums[1]!;
|
opts.max = nums[1]!;
|
||||||
} else {
|
} else if (nums.length > 2) {
|
||||||
showError(`uniform expects 0–2 args, got ${nums.length}: ${args.join(" ")}`);
|
showError(`uniform expects 0–2 args, got ${nums.length}: ${args.join(" ")}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "normal": {
|
case "normal": {
|
||||||
if (nums.length === 0) return;
|
|
||||||
if (nums.length === 1) {
|
if (nums.length === 1) {
|
||||||
opts.mean = nums[0]!;
|
opts.mean = nums[0]!;
|
||||||
} else if (nums.length === 2) {
|
} else if (nums.length === 2) {
|
||||||
opts.mean = nums[0]!;
|
opts.mean = nums[0]!;
|
||||||
opts.stddev = nums[1]!;
|
opts.stddev = nums[1]!;
|
||||||
} else {
|
} else if (nums.length > 2) {
|
||||||
showError(`normal expects 0–2 args, got ${nums.length}: ${args.join(" ")}`);
|
showError(`normal expects 0–2 args, got ${nums.length}: ${args.join(" ")}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "binomial": {
|
case "binomial": {
|
||||||
if (nums.length === 0) return;
|
|
||||||
if (nums.length === 1) {
|
if (nums.length === 1) {
|
||||||
opts.trials = nums[0]!;
|
opts.trials = nums[0]!;
|
||||||
} else if (nums.length === 2) {
|
} else if (nums.length === 2) {
|
||||||
opts.trials = nums[0]!;
|
opts.trials = nums[0]!;
|
||||||
opts.prob = nums[1]!;
|
opts.prob = nums[1]!;
|
||||||
} else {
|
} else if (nums.length > 2) {
|
||||||
showError(`binomial expects 0–2 args, got ${nums.length}: ${args.join(" ")}`);
|
showError(`binomial expects 0–2 args, got ${nums.length}: ${args.join(" ")}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "poisson": {
|
case "poisson": {
|
||||||
if (nums.length === 0) return;
|
|
||||||
if (nums.length === 1) {
|
if (nums.length === 1) {
|
||||||
opts.lambda = nums[0]!;
|
opts.lambda = nums[0]!;
|
||||||
} else {
|
} else if (nums.length > 1) {
|
||||||
showError(`poisson expects 0–1 arg, got ${nums.length}: ${args.join(" ")}`);
|
showError(`poisson expects 0–1 arg, got ${nums.length}: ${args.join(" ")}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "exponential": {
|
case "exponential": {
|
||||||
if (nums.length === 0) return;
|
|
||||||
if (nums.length === 1) {
|
if (nums.length === 1) {
|
||||||
opts.lambda = nums[0]!;
|
opts.lambda = nums[0]!;
|
||||||
} else {
|
} else if (nums.length > 1) {
|
||||||
showError(`exponential expects 0–1 arg, got ${nums.length}: ${args.join(" ")}`);
|
showError(`exponential expects 0–1 arg, got ${nums.length}: ${args.join(" ")}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "hypergeometric": {
|
case "hypergeometric": {
|
||||||
if (nums.length === 0) return;
|
|
||||||
if (nums.length >= 1) opts.popSize = nums[0]!;
|
if (nums.length >= 1) opts.popSize = nums[0]!;
|
||||||
if (nums.length >= 2) opts.successes = nums[1]!;
|
if (nums.length >= 2) opts.successes = nums[1]!;
|
||||||
if (nums.length >= 3) opts.draws = nums[2]!;
|
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 {
|
function parseNum(s: string): number {
|
||||||
const n = Number(s);
|
const n = Number(s);
|
||||||
if (isNaN(n)) showError(`not a number: ${s}`);
|
if (isNaN(n) || !isFinite(n)) showError(`not a number: ${s}`);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ async function main(): Promise<void> {
|
|||||||
const nums = parseStdinNumbers(input);
|
const nums = parseStdinNumbers(input);
|
||||||
if (nums.length > 0) {
|
if (nums.length > 0) {
|
||||||
applyPositionals(options, nums.map(String));
|
applyPositionals(options, nums.map(String));
|
||||||
|
} else if (input.trim()) {
|
||||||
|
console.error("rand: stdin contained non-numeric data, ignoring");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export function parseStdinNumbers(input: string): number[] {
|
|||||||
const nums: number[] = [];
|
const nums: number[] = [];
|
||||||
for (const p of parts) {
|
for (const p of parts) {
|
||||||
const n = Number(p);
|
const n = Number(p);
|
||||||
if (isNaN(n)) return []; // unrecognizable → ignore stdin
|
if (isNaN(n) || !isFinite(n)) return []; // unrecognizable → ignore stdin
|
||||||
nums.push(n);
|
nums.push(n);
|
||||||
}
|
}
|
||||||
return nums;
|
return nums;
|
||||||
|
|||||||
Reference in New Issue
Block a user