Compare commits

...

1 Commits

6 changed files with 32 additions and 27 deletions
+13 -13
View File
@@ -32,6 +32,7 @@ rand -f2 # 保留 2 位小数
| `-f[N]` | 小数位数:`-f` 默认 2 位,`-f1` = 1 位,`-f3` = 3 位 |
| `--float [N]` | `-f` 的长形式 |
| `-d, --dist <name>` | 概率分布,默认 `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)
```
## 构建
+2 -1
View File
@@ -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": {
+8
View File
@@ -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 0100");
});
test("-f with no arg defaults to 2", () => {
expect(parseArgs(["-f"]).options.decimals).toBe(2);
});
+6 -12
View File
@@ -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 02 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 02 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 02 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 01 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 01 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;
}
+2
View File
@@ -15,6 +15,8 @@ async function main(): Promise<void> {
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");
}
}
+1 -1
View File
@@ -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;