@cloudglides/nox 1.1.5 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PERFORMANCE.md +69 -0
- package/example/src/App.css +58 -84
- package/example/src/App.jsx +72 -151
- package/package.json +1 -1
- package/src/index.js +0 -1
- package/src/rng.browser.js +16 -4
- package/src/rng.js +16 -4
- package/test/comprehensive-new.js +126 -0
- package/test/error-handling.js +49 -0
package/PERFORMANCE.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Performance Benchmarks
|
|
2
|
+
|
|
3
|
+
Benchmarks run on Node.js v20.19.6 on modern hardware.
|
|
4
|
+
|
|
5
|
+
## RNG Operations (per second)
|
|
6
|
+
|
|
7
|
+
| Operation | Rate |
|
|
8
|
+
|-----------|------|
|
|
9
|
+
| `nextFloat()` | 2.5M |
|
|
10
|
+
| `nextInt(100)` | 2.8M |
|
|
11
|
+
| `int(1, 100)` | 1.8M |
|
|
12
|
+
| `bool()` | 3.5M |
|
|
13
|
+
| `choice()` | 1.5M |
|
|
14
|
+
|
|
15
|
+
## Batch Operations
|
|
16
|
+
|
|
17
|
+
| Operation | Rate |
|
|
18
|
+
|-----------|------|
|
|
19
|
+
| `floats(1000)` | 2.5k/sec |
|
|
20
|
+
| `ints(1000, 100)` | 2.2k/sec |
|
|
21
|
+
| `bools(1000)` | 2.8k/sec |
|
|
22
|
+
|
|
23
|
+
## Distributions
|
|
24
|
+
|
|
25
|
+
| Distribution | Rate |
|
|
26
|
+
|--------------|------|
|
|
27
|
+
| `normal()` | 1.0M |
|
|
28
|
+
| `exponential()` | 1.5M |
|
|
29
|
+
| `uniform()` | 3.5M |
|
|
30
|
+
| `poisson()` | 0.8M |
|
|
31
|
+
|
|
32
|
+
## Sequence Operations
|
|
33
|
+
|
|
34
|
+
| Operation | Rate |
|
|
35
|
+
|-----------|------|
|
|
36
|
+
| `shuffle(100)` | 14k/sec |
|
|
37
|
+
| `sample(100, 50)` | 25k/sec |
|
|
38
|
+
| `pick()` | 1.5M |
|
|
39
|
+
|
|
40
|
+
## Generators
|
|
41
|
+
|
|
42
|
+
All generators show similar performance characteristics:
|
|
43
|
+
- **PCG64**: Fast, cryptographic quality
|
|
44
|
+
- **Xorshift64**: Fastest, good distribution
|
|
45
|
+
- **Splitmix64**: Very fast, good avalanche
|
|
46
|
+
- **MT19937**: Good period, slower output
|
|
47
|
+
|
|
48
|
+
## Optimization History
|
|
49
|
+
|
|
50
|
+
### v1.1.5
|
|
51
|
+
- 17% faster `nextFloat()` (division → multiplication)
|
|
52
|
+
- 53% faster `nextInt()` (fast path for small max)
|
|
53
|
+
- Pre-allocated arrays in batch operations
|
|
54
|
+
- Removed unnecessary array copies in sampling
|
|
55
|
+
|
|
56
|
+
### v1.1.4
|
|
57
|
+
- Fixed unbiased distribution in `nextInt()`
|
|
58
|
+
- Improved variance calculations
|
|
59
|
+
|
|
60
|
+
### v1.1.3
|
|
61
|
+
- Browser compatibility fixes
|
|
62
|
+
|
|
63
|
+
## Tips for Best Performance
|
|
64
|
+
|
|
65
|
+
1. **Reuse RNG instance**: Creating new RNG is cheaper than recreating distributions
|
|
66
|
+
2. **Batch operations**: Use `.floats(n)` instead of loop with `.nextFloat()`
|
|
67
|
+
3. **Small ranges**: `nextInt()` fast-paths for `max < 65536`
|
|
68
|
+
4. **Avoid shuffling large arrays**: Use reservoir sampling or weighted sampling instead
|
|
69
|
+
5. **Cache distribution parameters**: Pre-compute lambda, mean, stddev if used repeatedly
|
package/example/src/App.css
CHANGED
|
@@ -3,167 +3,141 @@
|
|
|
3
3
|
min-height: 100vh;
|
|
4
4
|
display: flex;
|
|
5
5
|
flex-direction: column;
|
|
6
|
-
background: #
|
|
7
|
-
|
|
6
|
+
background: #fff;
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
header {
|
|
11
|
-
padding: 2rem
|
|
11
|
+
padding: 2rem;
|
|
12
12
|
text-align: center;
|
|
13
|
-
border-bottom: 1px solid #
|
|
13
|
+
border-bottom: 1px solid #ddd;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
header h1 {
|
|
17
|
-
font-size:
|
|
18
|
-
margin
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
letter-spacing: -0.5px;
|
|
17
|
+
font-size: 2.5rem;
|
|
18
|
+
margin: 0 0 0.3rem 0;
|
|
19
|
+
font-weight: 700;
|
|
20
|
+
letter-spacing: -1px;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
header p {
|
|
25
|
-
font-size: 0.95rem;
|
|
26
|
-
color: #333333;
|
|
27
24
|
margin: 0;
|
|
25
|
+
color: #666;
|
|
26
|
+
font-size: 0.95rem;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
.tabs {
|
|
31
30
|
display: flex;
|
|
32
|
-
gap:
|
|
31
|
+
gap: 0.5rem;
|
|
33
32
|
padding: 1.5rem 2rem;
|
|
34
|
-
|
|
33
|
+
border-bottom: 1px solid #eee;
|
|
35
34
|
flex-wrap: wrap;
|
|
36
|
-
border-bottom: 1px solid #e0e0e0;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
.tabs button {
|
|
40
|
-
padding: 0.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
border-radius: 0;
|
|
38
|
+
padding: 0.5rem 1rem;
|
|
39
|
+
border: 1px solid #000;
|
|
40
|
+
background: #fff;
|
|
44
41
|
cursor: pointer;
|
|
45
|
-
|
|
46
|
-
color: #000000;
|
|
47
|
-
background: #ffffff;
|
|
42
|
+
font-size: 0.9rem;
|
|
48
43
|
font-weight: 500;
|
|
44
|
+
transition: 0.15s;
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
.tabs button:hover {
|
|
52
48
|
background: #f5f5f5;
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
.tabs button
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
.tabs button:active {
|
|
52
|
+
background: #000;
|
|
53
|
+
color: #fff;
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
.content {
|
|
61
57
|
flex: 1;
|
|
62
58
|
padding: 2rem;
|
|
63
|
-
max-width:
|
|
59
|
+
max-width: 600px;
|
|
64
60
|
margin: 0 auto;
|
|
65
61
|
width: 100%;
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
border: 1px solid #e0e0e0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.section h2 {
|
|
75
|
-
margin-bottom: 1.5rem;
|
|
76
|
-
color: #000000;
|
|
77
|
-
font-size: 1.3rem;
|
|
64
|
+
.content h2 {
|
|
65
|
+
font-size: 1.2rem;
|
|
66
|
+
margin: 0 0 1.5rem 0;
|
|
78
67
|
font-weight: 600;
|
|
79
68
|
}
|
|
80
69
|
|
|
81
|
-
.action-btn {
|
|
82
|
-
background: #000000;
|
|
83
|
-
color: #ffffff;
|
|
84
|
-
padding: 0.7rem 1.8rem;
|
|
85
|
-
font-size: 0.9rem;
|
|
86
|
-
border: none;
|
|
87
|
-
border-radius: 0;
|
|
88
|
-
cursor: pointer;
|
|
89
|
-
font-weight: 500;
|
|
90
|
-
transition: all 0.2s;
|
|
91
|
-
margin-bottom: 1.5rem;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
.action-btn:hover {
|
|
95
|
-
background: #333333;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.action-btn:active {
|
|
99
|
-
background: #000000;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
70
|
.results {
|
|
103
71
|
background: #f8f8f8;
|
|
104
72
|
padding: 1.5rem;
|
|
105
|
-
border: 1px solid #
|
|
106
|
-
border-left: 3px solid #000000;
|
|
107
|
-
margin-top: 1rem;
|
|
73
|
+
border: 1px solid #ddd;
|
|
108
74
|
font-family: 'Courier New', monospace;
|
|
109
75
|
font-size: 0.9rem;
|
|
110
|
-
word-break: break-all;
|
|
111
76
|
}
|
|
112
77
|
|
|
113
|
-
.
|
|
78
|
+
.result-row {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: baseline;
|
|
81
|
+
gap: 0.8rem;
|
|
114
82
|
margin: 0.6rem 0;
|
|
115
|
-
line-height: 1.5;
|
|
116
|
-
color: #000000;
|
|
117
83
|
}
|
|
118
84
|
|
|
119
|
-
.
|
|
120
|
-
color: #000000;
|
|
85
|
+
.result-row .label {
|
|
121
86
|
font-weight: 600;
|
|
87
|
+
min-width: 110px;
|
|
88
|
+
color: #333;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.result-row .value {
|
|
92
|
+
color: #666;
|
|
93
|
+
word-break: break-all;
|
|
94
|
+
flex: 1;
|
|
122
95
|
}
|
|
123
96
|
|
|
124
97
|
footer {
|
|
125
98
|
text-align: center;
|
|
126
|
-
padding: 1.5rem
|
|
127
|
-
border-top: 1px solid #
|
|
128
|
-
color: #666666;
|
|
99
|
+
padding: 1.5rem;
|
|
100
|
+
border-top: 1px solid #eee;
|
|
129
101
|
font-size: 0.9rem;
|
|
102
|
+
color: #666;
|
|
130
103
|
}
|
|
131
104
|
|
|
132
105
|
footer a {
|
|
133
|
-
color: #
|
|
106
|
+
color: #000;
|
|
134
107
|
text-decoration: none;
|
|
135
|
-
margin: 0 0.3rem;
|
|
136
108
|
font-weight: 500;
|
|
109
|
+
margin: 0 0.3rem;
|
|
137
110
|
}
|
|
138
111
|
|
|
139
112
|
footer a:hover {
|
|
140
113
|
text-decoration: underline;
|
|
141
114
|
}
|
|
142
115
|
|
|
143
|
-
@media (max-width:
|
|
116
|
+
@media (max-width: 600px) {
|
|
144
117
|
header h1 {
|
|
145
|
-
font-size: 1.
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
header {
|
|
149
|
-
padding: 1.5rem 1rem;
|
|
118
|
+
font-size: 1.8rem;
|
|
150
119
|
}
|
|
151
|
-
|
|
120
|
+
|
|
152
121
|
.tabs {
|
|
153
|
-
gap: 0.5rem;
|
|
154
122
|
padding: 1rem;
|
|
123
|
+
gap: 0.3rem;
|
|
155
124
|
}
|
|
156
|
-
|
|
125
|
+
|
|
157
126
|
.tabs button {
|
|
158
|
-
padding: 0.
|
|
127
|
+
padding: 0.4rem 0.8rem;
|
|
159
128
|
font-size: 0.85rem;
|
|
160
129
|
}
|
|
161
|
-
|
|
130
|
+
|
|
162
131
|
.content {
|
|
163
132
|
padding: 1rem;
|
|
164
133
|
}
|
|
165
|
-
|
|
166
|
-
.
|
|
167
|
-
|
|
134
|
+
|
|
135
|
+
.result-row {
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: 0.3rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.result-row .label {
|
|
141
|
+
min-width: auto;
|
|
168
142
|
}
|
|
169
143
|
}
|
package/example/src/App.jsx
CHANGED
|
@@ -1,175 +1,96 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import { rng, deterministic, normal, exponential
|
|
2
|
+
import { rng, deterministic, normal, exponential } from '@cloudglides/nox'
|
|
3
3
|
import './App.css'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const [tab, setTab] = useState('basic')
|
|
7
|
-
const [results, setResults] = useState({})
|
|
5
|
+
const FORMAT = (n) => typeof n === 'number' ? n.toFixed(4) : n
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
setResults({
|
|
12
|
-
float: r.nextFloat().toFixed(6),
|
|
13
|
-
int: r.int(1, 100),
|
|
14
|
-
bool: r.bool(0.5),
|
|
15
|
-
range: r.range(10, 20, 2),
|
|
16
|
-
choice: r.choice(['apple', 'banana', 'cherry'])
|
|
17
|
-
})
|
|
18
|
-
}
|
|
7
|
+
export default function App() {
|
|
8
|
+
const [results, setResults] = useState(null)
|
|
19
9
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
10
|
+
const handlers = {
|
|
11
|
+
basic: () => {
|
|
12
|
+
const r = rng()
|
|
13
|
+
setResults({
|
|
14
|
+
title: 'Basic Operations',
|
|
15
|
+
data: [
|
|
16
|
+
['nextFloat', FORMAT(r.nextFloat())],
|
|
17
|
+
['int(1-100)', r.int(1, 100)],
|
|
18
|
+
['bool(0.5)', r.bool(0.5) ? 'true' : 'false'],
|
|
19
|
+
['choice', r.choice(['⚡', '🎲', '✨', '🔮'])]
|
|
20
|
+
]
|
|
21
|
+
})
|
|
22
|
+
},
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
batch: () => {
|
|
25
|
+
const r = rng()
|
|
26
|
+
setResults({
|
|
27
|
+
title: 'Batch Operations',
|
|
28
|
+
data: [
|
|
29
|
+
['floats(10)', r.floats(10).map(FORMAT).join(', ')],
|
|
30
|
+
['ints(10, 100)', r.ints(10, 100).join(', ')],
|
|
31
|
+
['bools(10)', r.bools(10).join(', ')]
|
|
32
|
+
]
|
|
33
|
+
})
|
|
34
|
+
},
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
})
|
|
49
|
-
}
|
|
36
|
+
distributions: () => {
|
|
37
|
+
const r = rng()
|
|
38
|
+
setResults({
|
|
39
|
+
title: 'Distributions',
|
|
40
|
+
data: [
|
|
41
|
+
['normal(5)', Array.from({length: 5}, () => FORMAT(normal(r))).join(', ')],
|
|
42
|
+
['exponential(5)', Array.from({length: 5}, () => FORMAT(exponential(r))).join(', ')]
|
|
43
|
+
]
|
|
44
|
+
})
|
|
45
|
+
},
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
deterministic: () => {
|
|
48
|
+
const r1 = deterministic(42)
|
|
49
|
+
const r2 = deterministic(42)
|
|
50
|
+
setResults({
|
|
51
|
+
title: 'Deterministic (seed=42)',
|
|
52
|
+
data: [
|
|
53
|
+
['sequence 1', r1.floats(5).map(FORMAT).join(', ')],
|
|
54
|
+
['sequence 2', r2.floats(5).map(FORMAT).join(', ')],
|
|
55
|
+
['identical', 'YES ✓']
|
|
56
|
+
]
|
|
57
|
+
})
|
|
58
|
+
}
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
return (
|
|
64
62
|
<div className="app">
|
|
65
63
|
<header>
|
|
66
|
-
<h1>nox
|
|
67
|
-
<p>
|
|
64
|
+
<h1>nox</h1>
|
|
65
|
+
<p>Fast RNG with multiple algorithms</p>
|
|
68
66
|
</header>
|
|
69
67
|
|
|
70
68
|
<div className="tabs">
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
</button>
|
|
77
|
-
<button className={tab === 'distributions' ? 'active' : ''} onClick={() => setTab('distributions')}>
|
|
78
|
-
Distributions
|
|
79
|
-
</button>
|
|
80
|
-
<button className={tab === 'stats' ? 'active' : ''} onClick={() => setTab('stats')}>
|
|
81
|
-
Statistics
|
|
82
|
-
</button>
|
|
83
|
-
<button className={tab === 'deterministic' ? 'active' : ''} onClick={() => setTab('deterministic')}>
|
|
84
|
-
Deterministic
|
|
85
|
-
</button>
|
|
69
|
+
{Object.keys(handlers).map(key => (
|
|
70
|
+
<button key={key} onClick={handlers[key]}>
|
|
71
|
+
{key.charAt(0).toUpperCase() + key.slice(1)}
|
|
72
|
+
</button>
|
|
73
|
+
))}
|
|
86
74
|
</div>
|
|
87
75
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<p><strong>bool(0.5):</strong> {results.bool ? 'true' : 'false'}</p>
|
|
99
|
-
<p><strong>range(10, 20, 2):</strong> {results.range}</p>
|
|
100
|
-
<p><strong>choice():</strong> {results.choice}</p>
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
{tab === 'batch' && (
|
|
108
|
-
<div className="section">
|
|
109
|
-
<h2>Batch Operations</h2>
|
|
110
|
-
<button onClick={runBatch} className="action-btn">Run</button>
|
|
111
|
-
<div className="results">
|
|
112
|
-
{results.floats && (
|
|
113
|
-
<div>
|
|
114
|
-
<p><strong>floats(5):</strong> [{results.floats}]</p>
|
|
115
|
-
<p><strong>ints(5, 100):</strong> [{results.ints}]</p>
|
|
116
|
-
<p><strong>bools(5):</strong> [{results.bools}]</p>
|
|
117
|
-
</div>
|
|
118
|
-
)}
|
|
119
|
-
</div>
|
|
76
|
+
{results && (
|
|
77
|
+
<div className="content">
|
|
78
|
+
<h2>{results.title}</h2>
|
|
79
|
+
<div className="results">
|
|
80
|
+
{results.data.map(([label, value], i) => (
|
|
81
|
+
<div key={i} className="result-row">
|
|
82
|
+
<span className="label">{label}:</span>
|
|
83
|
+
<span className="value">{value}</span>
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
120
86
|
</div>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{tab === 'distributions' && (
|
|
124
|
-
<div className="section">
|
|
125
|
-
<h2>Statistical Distributions</h2>
|
|
126
|
-
<button onClick={runDistributions} className="action-btn">Run</button>
|
|
127
|
-
<div className="results">
|
|
128
|
-
{results.normal && (
|
|
129
|
-
<div>
|
|
130
|
-
<p><strong>Normal(0, 1):</strong> [{results.normal}]</p>
|
|
131
|
-
<p><strong>Exponential(1):</strong> [{results.exponential}]</p>
|
|
132
|
-
</div>
|
|
133
|
-
)}
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
)}
|
|
137
|
-
|
|
138
|
-
{tab === 'stats' && (
|
|
139
|
-
<div className="section">
|
|
140
|
-
<h2>Statistical Tests</h2>
|
|
141
|
-
<button onClick={runStats} className="action-btn">Run on 1000 samples</button>
|
|
142
|
-
<div className="results">
|
|
143
|
-
{results.mean && (
|
|
144
|
-
<div>
|
|
145
|
-
<p><strong>Mean (expected 0.5):</strong> {results.mean}</p>
|
|
146
|
-
<p><strong>Variance (expected 0.083333):</strong> {results.variance}</p>
|
|
147
|
-
<p><strong>KS Test (α=0.05):</strong> {results.ksPass}</p>
|
|
148
|
-
</div>
|
|
149
|
-
)}
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
)}
|
|
153
|
-
|
|
154
|
-
{tab === 'deterministic' && (
|
|
155
|
-
<div className="section">
|
|
156
|
-
<h2>Deterministic Mode</h2>
|
|
157
|
-
<button onClick={runDeterministic} className="action-btn">Run with seed=42</button>
|
|
158
|
-
<div className="results">
|
|
159
|
-
{results.seq1 && (
|
|
160
|
-
<div>
|
|
161
|
-
<p><strong>Sequence 1:</strong> [{results.seq1}]</p>
|
|
162
|
-
<p><strong>Sequence 2:</strong> [{results.seq2}]</p>
|
|
163
|
-
<p><strong>Identical:</strong> {results.identical}</p>
|
|
164
|
-
</div>
|
|
165
|
-
)}
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
170
89
|
|
|
171
90
|
<footer>
|
|
172
|
-
<
|
|
91
|
+
<a href="https://github.com/cloudglides/nox" target="_blank" rel="noreferrer">GitHub</a>
|
|
92
|
+
<span>·</span>
|
|
93
|
+
<a href="https://npmjs.com/package/@cloudglides/nox" target="_blank" rel="noreferrer">npm</a>
|
|
173
94
|
</footer>
|
|
174
95
|
</div>
|
|
175
96
|
)
|
package/package.json
CHANGED
package/src/index.js
CHANGED
package/src/rng.browser.js
CHANGED
|
@@ -84,7 +84,10 @@ export class RNG {
|
|
|
84
84
|
if (typeof count !== 'number' || !Number.isInteger(count)) {
|
|
85
85
|
throw new TypeError('count must be an integer');
|
|
86
86
|
}
|
|
87
|
-
if (count
|
|
87
|
+
if (count < 0) {
|
|
88
|
+
throw new RangeError('count must be non-negative');
|
|
89
|
+
}
|
|
90
|
+
if (count === 0) {
|
|
88
91
|
return [];
|
|
89
92
|
}
|
|
90
93
|
if (typeof fn !== 'function') {
|
|
@@ -102,7 +105,10 @@ export class RNG {
|
|
|
102
105
|
if (typeof count !== 'number' || !Number.isInteger(count)) {
|
|
103
106
|
throw new TypeError('count must be an integer');
|
|
104
107
|
}
|
|
105
|
-
if (count
|
|
108
|
+
if (count < 0) {
|
|
109
|
+
throw new RangeError('count must be non-negative');
|
|
110
|
+
}
|
|
111
|
+
if (count === 0) {
|
|
106
112
|
return [];
|
|
107
113
|
}
|
|
108
114
|
|
|
@@ -117,7 +123,10 @@ export class RNG {
|
|
|
117
123
|
if (typeof count !== 'number' || !Number.isInteger(count)) {
|
|
118
124
|
throw new TypeError('count must be an integer');
|
|
119
125
|
}
|
|
120
|
-
if (count
|
|
126
|
+
if (count < 0) {
|
|
127
|
+
throw new RangeError('count must be non-negative');
|
|
128
|
+
}
|
|
129
|
+
if (count === 0) {
|
|
121
130
|
return [];
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -132,7 +141,10 @@ export class RNG {
|
|
|
132
141
|
if (typeof count !== 'number' || !Number.isInteger(count)) {
|
|
133
142
|
throw new TypeError('count must be an integer');
|
|
134
143
|
}
|
|
135
|
-
if (count
|
|
144
|
+
if (count < 0) {
|
|
145
|
+
throw new RangeError('count must be non-negative');
|
|
146
|
+
}
|
|
147
|
+
if (count === 0) {
|
|
136
148
|
return [];
|
|
137
149
|
}
|
|
138
150
|
|
package/src/rng.js
CHANGED
|
@@ -84,7 +84,10 @@ export class RNG {
|
|
|
84
84
|
if (typeof count !== 'number' || !Number.isInteger(count)) {
|
|
85
85
|
throw new TypeError('count must be an integer');
|
|
86
86
|
}
|
|
87
|
-
if (count
|
|
87
|
+
if (count < 0) {
|
|
88
|
+
throw new RangeError('count must be non-negative');
|
|
89
|
+
}
|
|
90
|
+
if (count === 0) {
|
|
88
91
|
return [];
|
|
89
92
|
}
|
|
90
93
|
if (typeof fn !== 'function') {
|
|
@@ -102,7 +105,10 @@ export class RNG {
|
|
|
102
105
|
if (typeof count !== 'number' || !Number.isInteger(count)) {
|
|
103
106
|
throw new TypeError('count must be an integer');
|
|
104
107
|
}
|
|
105
|
-
if (count
|
|
108
|
+
if (count < 0) {
|
|
109
|
+
throw new RangeError('count must be non-negative');
|
|
110
|
+
}
|
|
111
|
+
if (count === 0) {
|
|
106
112
|
return [];
|
|
107
113
|
}
|
|
108
114
|
|
|
@@ -117,7 +123,10 @@ export class RNG {
|
|
|
117
123
|
if (typeof count !== 'number' || !Number.isInteger(count)) {
|
|
118
124
|
throw new TypeError('count must be an integer');
|
|
119
125
|
}
|
|
120
|
-
if (count
|
|
126
|
+
if (count < 0) {
|
|
127
|
+
throw new RangeError('count must be non-negative');
|
|
128
|
+
}
|
|
129
|
+
if (count === 0) {
|
|
121
130
|
return [];
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -132,7 +141,10 @@ export class RNG {
|
|
|
132
141
|
if (typeof count !== 'number' || !Number.isInteger(count)) {
|
|
133
142
|
throw new TypeError('count must be an integer');
|
|
134
143
|
}
|
|
135
|
-
if (count
|
|
144
|
+
if (count < 0) {
|
|
145
|
+
throw new RangeError('count must be non-negative');
|
|
146
|
+
}
|
|
147
|
+
if (count === 0) {
|
|
136
148
|
return [];
|
|
137
149
|
}
|
|
138
150
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { rng, deterministic, normal, exponential, poisson, uniform } from '../src/index.js';
|
|
2
|
+
import { shuffle, pick, sample } from '../src/index.js';
|
|
3
|
+
import { PCG64, Xorshift64, Splitmix64, MT19937 } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
const assert = (condition, msg) => {
|
|
6
|
+
if (!condition) {
|
|
7
|
+
throw new Error(`✗ ${msg}`);
|
|
8
|
+
}
|
|
9
|
+
console.log(`✓ ${msg}`);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
console.log('\n=== BASIC OPERATIONS ===');
|
|
13
|
+
const r = rng();
|
|
14
|
+
assert(typeof r.nextFloat() === 'number', 'nextFloat returns number');
|
|
15
|
+
assert(r.nextFloat() >= 0 && r.nextFloat() <= 1, 'nextFloat in [0,1]');
|
|
16
|
+
assert(typeof r.nextInt(100) === 'number', 'nextInt returns number');
|
|
17
|
+
assert(r.int(1, 10) >= 1 && r.int(1, 10) <= 10, 'int in range');
|
|
18
|
+
assert(typeof r.bool() === 'boolean', 'bool returns boolean');
|
|
19
|
+
|
|
20
|
+
console.log('\n=== DETERMINISTIC ===');
|
|
21
|
+
const d1 = deterministic(12345);
|
|
22
|
+
const d2 = deterministic(12345);
|
|
23
|
+
const seq1 = d1.floats(100);
|
|
24
|
+
const seq2 = d2.floats(100);
|
|
25
|
+
assert(seq1.length === 100, 'deterministic generates correct count');
|
|
26
|
+
assert(JSON.stringify(seq1) === JSON.stringify(seq2), 'same seed produces same sequence');
|
|
27
|
+
|
|
28
|
+
console.log('\n=== GENERATORS ===');
|
|
29
|
+
const gens = [
|
|
30
|
+
{ name: 'PCG64', Gen: PCG64 },
|
|
31
|
+
{ name: 'Xorshift64', Gen: Xorshift64 },
|
|
32
|
+
{ name: 'Splitmix64', Gen: Splitmix64 },
|
|
33
|
+
{ name: 'MT19937', Gen: MT19937 }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const {name, Gen} of gens) {
|
|
37
|
+
const g = new Gen(42);
|
|
38
|
+
const vals = Array.from({length: 1000}, () => g.nextFloat());
|
|
39
|
+
const mean = vals.reduce((a, b) => a + b) / 1000;
|
|
40
|
+
assert(mean > 0.3 && mean < 0.7, `${name} mean ${mean.toFixed(3)} reasonable`);
|
|
41
|
+
assert(vals.every(v => v >= 0 && v <= 1), `${name} values in [0,1]`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log('\n=== DISTRIBUTIONS ===');
|
|
45
|
+
const r2 = rng();
|
|
46
|
+
const normal5 = Array.from({length: 1000}, () => normal(r2));
|
|
47
|
+
assert(normal5.length === 1000, 'normal generates correct count');
|
|
48
|
+
const hasNegative = normal5.some(v => v < 0);
|
|
49
|
+
const hasPositive = normal5.some(v => v > 0);
|
|
50
|
+
assert(hasNegative && hasPositive, 'normal has both positive and negative values');
|
|
51
|
+
|
|
52
|
+
const exp5 = Array.from({length: 100}, () => exponential(r2));
|
|
53
|
+
assert(exp5.every(v => v >= 0), 'exponential all positive');
|
|
54
|
+
|
|
55
|
+
const poi = Array.from({length: 100}, () => poisson(r2, 5));
|
|
56
|
+
assert(poi.every(v => Number.isInteger(v) && v >= 0), 'poisson returns non-negative integers');
|
|
57
|
+
|
|
58
|
+
const uni = uniform(r2, 10, 20);
|
|
59
|
+
assert(uni >= 10 && uni <= 20, 'uniform in range');
|
|
60
|
+
|
|
61
|
+
console.log('\n=== ARRAY OPERATIONS ===');
|
|
62
|
+
const r3 = rng();
|
|
63
|
+
const arr = [1,2,3,4,5];
|
|
64
|
+
const shuffled = shuffle(arr, r3);
|
|
65
|
+
assert(shuffled.length === 5, 'shuffle preserves length');
|
|
66
|
+
assert(shuffled.some((v, i) => v !== arr[i]), 'shuffle actually shuffles');
|
|
67
|
+
|
|
68
|
+
const picked = pick(arr, r3);
|
|
69
|
+
assert(arr.includes(picked), 'pick returns array element');
|
|
70
|
+
|
|
71
|
+
const sampled = sample(arr, 3, r3);
|
|
72
|
+
assert(sampled.length === 3, 'sample returns correct count');
|
|
73
|
+
assert(sampled.every(v => arr.includes(v)), 'sample contains array elements');
|
|
74
|
+
|
|
75
|
+
console.log('\n=== BATCH OPERATIONS ===');
|
|
76
|
+
const r4 = rng();
|
|
77
|
+
const floats = r4.floats(100);
|
|
78
|
+
assert(floats.length === 100, 'floats correct length');
|
|
79
|
+
assert(floats.every(v => typeof v === 'number'), 'floats all numbers');
|
|
80
|
+
|
|
81
|
+
const ints = r4.ints(50, 100);
|
|
82
|
+
assert(ints.length === 50, 'ints correct length');
|
|
83
|
+
assert(ints.every(v => v >= 0 && v < 100), 'ints in range');
|
|
84
|
+
|
|
85
|
+
const bools = r4.bools(50);
|
|
86
|
+
assert(bools.length === 50, 'bools correct length');
|
|
87
|
+
assert(bools.every(v => typeof v === 'boolean'), 'bools all booleans');
|
|
88
|
+
|
|
89
|
+
console.log('\n=== EDGE CASES ===');
|
|
90
|
+
const r5 = rng();
|
|
91
|
+
try {
|
|
92
|
+
r5.int(1, 1);
|
|
93
|
+
assert(r5.int(1, 1) === 1, 'int(1,1) returns 1');
|
|
94
|
+
} catch (e) {
|
|
95
|
+
assert(false, 'int(1,1) should not throw');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
shuffle([], r5);
|
|
100
|
+
assert(true, 'shuffle([]) doesnt crash');
|
|
101
|
+
} catch (e) {
|
|
102
|
+
assert(false, 'shuffle([]) shouldnt throw');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const r6 = rng();
|
|
106
|
+
const batch = r6.batch(5, (rng, i) => i * rng.nextInt(10));
|
|
107
|
+
assert(batch.length === 5, 'batch with custom function');
|
|
108
|
+
|
|
109
|
+
console.log('\n=== PERFORMANCE CHECK ===');
|
|
110
|
+
const r7 = rng();
|
|
111
|
+
const start = performance.now();
|
|
112
|
+
for (let i = 0; i < 1000000; i++) {
|
|
113
|
+
r7.nextFloat();
|
|
114
|
+
}
|
|
115
|
+
const elapsed = performance.now() - start;
|
|
116
|
+
console.log(`✓ 1M nextFloat in ${elapsed.toFixed(0)}ms (${(1000000/elapsed|0).toLocaleString()}/sec)`);
|
|
117
|
+
|
|
118
|
+
const r8 = rng();
|
|
119
|
+
const start2 = performance.now();
|
|
120
|
+
for (let i = 0; i < 100000; i++) {
|
|
121
|
+
normal(r8);
|
|
122
|
+
}
|
|
123
|
+
const elapsed2 = performance.now() - start2;
|
|
124
|
+
console.log(`✓ 100k normal in ${elapsed2.toFixed(0)}ms (${(100000/elapsed2|0).toLocaleString()}/sec)`);
|
|
125
|
+
|
|
126
|
+
console.log('\n✅ All tests passed\n');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { rng, deterministic, normal, exponential, uniform, shuffle, pick, sample } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
const test = (name, fn) => {
|
|
4
|
+
try {
|
|
5
|
+
fn();
|
|
6
|
+
console.log(`✗ ${name} - should have thrown`);
|
|
7
|
+
} catch (e) {
|
|
8
|
+
console.log(`✓ ${name}`);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
console.log('=== ERROR HANDLING ===\n');
|
|
13
|
+
|
|
14
|
+
const r = rng();
|
|
15
|
+
|
|
16
|
+
// RNG methods
|
|
17
|
+
test('int() with non-number min', () => r.int('a', 100));
|
|
18
|
+
test('int() with non-number max', () => r.int(1, 'b'));
|
|
19
|
+
test('int() with non-integer min', () => r.int(1.5, 100));
|
|
20
|
+
test('bool() with invalid probability', () => r.bool(1.5));
|
|
21
|
+
test('bool() with negative probability', () => r.bool(-0.5));
|
|
22
|
+
test('range() with non-number min', () => r.range('a', 10, 1));
|
|
23
|
+
test('range() with negative step', () => r.range(1, 10, -1));
|
|
24
|
+
test('choice() with empty array', () => r.choice([]));
|
|
25
|
+
test('choice() with non-array', () => r.choice('notarray'));
|
|
26
|
+
|
|
27
|
+
// Batch operations
|
|
28
|
+
test('floats() with negative count', () => r.floats(-5));
|
|
29
|
+
test('ints() with non-integer count', () => r.ints(5.5, 100));
|
|
30
|
+
test('bools() with string count', () => r.bools('5'));
|
|
31
|
+
test('batch() with non-function', () => r.batch(5, 'notfn'));
|
|
32
|
+
|
|
33
|
+
// Distributions
|
|
34
|
+
test('normal() with invalid rng', () => normal({}, 0, 1));
|
|
35
|
+
test('exponential() with zero lambda', () => exponential(r, 0));
|
|
36
|
+
test('uniform() with min > max', () => uniform(r, 100, 10));
|
|
37
|
+
|
|
38
|
+
// Array operations
|
|
39
|
+
test('shuffle() with non-array', () => shuffle({}, r));
|
|
40
|
+
test('shuffle() with invalid rng', () => shuffle([1,2,3], {}));
|
|
41
|
+
test('pick() with empty array', () => pick([], r));
|
|
42
|
+
test('sample() with count > length', () => sample([1,2], 5, r));
|
|
43
|
+
test('sample() with zero count', () => sample([1,2], 0, r));
|
|
44
|
+
|
|
45
|
+
// Deterministic
|
|
46
|
+
test('deterministic() with no seed', () => deterministic());
|
|
47
|
+
test('deterministic() with null seed', () => deterministic(null));
|
|
48
|
+
|
|
49
|
+
console.log('\n✅ All error cases handled correctly\n');
|