pseudo_random 1.0.0
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.
- checksums.yaml +7 -0
- data/.rubocop.yml +50 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE.txt +21 -0
- data/README.md +290 -0
- data/Rakefile +12 -0
- data/lib/pseudo_random/version.rb +5 -0
- data/lib/pseudo_random.rb +268 -0
- data/sig/pseudo_random.rbs +24 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 90e087be024b1baf1841f93924e3d1303ed1b131f8ed63a676115db7d6317f02
|
|
4
|
+
data.tar.gz: b305efbe151f6675b163f04eb7671264c865f6fe9f283f73afcd3416862e24c3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d3a8678024b1ad7be9e5af9e85fcceffa7a3517fed4c64d50275fe16973892f7b414cd84b1007c0f87ecf3fcda33d1fd953a87128c879643ef1ce3818a44c278
|
|
7
|
+
data.tar.gz: 95d0bba372eabb184cf433dddd6f38ef3d987b48d265cf18501b577e9971b84180aa34bc2755309d64a574ded98730e475b5416d3d6a7ddcfce91caf3ff547e2
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
Exclude:
|
|
4
|
+
- 'pseudo_random.gemspec'
|
|
5
|
+
|
|
6
|
+
##################### Layout ##################################
|
|
7
|
+
Layout/LineLength:
|
|
8
|
+
Max: 120
|
|
9
|
+
|
|
10
|
+
Layout/EndOfLine:
|
|
11
|
+
EnforcedStyle: lf
|
|
12
|
+
|
|
13
|
+
Layout/EmptyLinesAroundAttributeAccessor:
|
|
14
|
+
Enabled: true
|
|
15
|
+
|
|
16
|
+
Layout/SpaceAroundMethodCallOperator:
|
|
17
|
+
Enabled: true
|
|
18
|
+
|
|
19
|
+
##################### Style ###################################
|
|
20
|
+
Style/Documentation:
|
|
21
|
+
Enabled: false
|
|
22
|
+
|
|
23
|
+
Style/StringLiterals:
|
|
24
|
+
EnforcedStyle: single_quotes
|
|
25
|
+
|
|
26
|
+
Style/StringLiteralsInInterpolation:
|
|
27
|
+
EnforcedStyle: single_quotes
|
|
28
|
+
|
|
29
|
+
Style/NumericPredicate:
|
|
30
|
+
Enabled: false
|
|
31
|
+
|
|
32
|
+
##################### Naming #################################
|
|
33
|
+
Naming/VariableNumber:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
##################### Metrics #################################
|
|
37
|
+
Metrics/ClassLength:
|
|
38
|
+
Max: 200
|
|
39
|
+
|
|
40
|
+
Metrics/MethodLength:
|
|
41
|
+
Max: 80
|
|
42
|
+
|
|
43
|
+
Metrics/AbcSize:
|
|
44
|
+
Max: 70
|
|
45
|
+
|
|
46
|
+
Metrics/CyclomaticComplexity:
|
|
47
|
+
Max: 20
|
|
48
|
+
|
|
49
|
+
Metrics/PerceivedComplexity:
|
|
50
|
+
Max: 15
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
This project adheres to [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
Versioning policy (summary):
|
|
7
|
+
|
|
8
|
+
- MAJOR: Backward-incompatible changes (breaking API changes, method signature changes, or any change that alters the deterministic output sequence for an identical seed / call order without being an explicitly documented bug fix).
|
|
9
|
+
- MINOR: Backward-compatible feature additions (new methods, new optional parameters, etc.). Existing deterministic sequences for existing seeds remain stable (except when corrected by a PATCH-level bug fix).
|
|
10
|
+
- PATCH: Backward-compatible bug fixes, internal improvements, or documentation-only changes. Public API and existing deterministic output sequences are not modified (unless prior behavior was incorrect per documentation—in such cases the CHANGELOG will call it out explicitly as a fix).
|
|
11
|
+
- Pre-release (e.g., 1.1.0-alpha.1): Experimental; output sequence stability is not guaranteed until the final release.
|
|
12
|
+
|
|
13
|
+
Deterministic output compatibility: the mapping (seed, invocation order) -> value is part of the public API. Changing the underlying algorithm is treated as a MAJOR change unless clearly marked and justified as a bug fix.
|
|
14
|
+
|
|
15
|
+
Deprecations: A deprecated feature will remain for at least one MINOR release after the deprecation notice before removal in the next MAJOR release.
|
|
16
|
+
|
|
17
|
+
## [Unreleased]
|
|
18
|
+
|
|
19
|
+
### 追加
|
|
20
|
+
|
|
21
|
+
- ここに未リリースの変更を追記してください。
|
|
22
|
+
|
|
23
|
+
## [1.0.0] - 2025-08-14
|
|
24
|
+
|
|
25
|
+
### 追加
|
|
26
|
+
|
|
27
|
+
- 初回リリース: 決定的な擬似乱数ジェネレータ (数値 / hex / alphabetic / alphanumeric 文字列生成)
|
|
28
|
+
- 任意オブジェクトシード対応
|
|
29
|
+
|
|
30
|
+
[Unreleased]: https://github.com/aYosukeMakita/pseudo_random/compare/v1.0.0...HEAD
|
|
31
|
+
[1.0.0]: https://github.com/aYosukeMakita/pseudo_random/releases/tag/v1.0.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 aYosukeMakita
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# PseudoRandom
|
|
2
|
+
|
|
3
|
+
PseudoRandom is a Ruby library that generates deterministic, reproducible pseudo-random numbers from a seed. Given the same seed, it always produces the same sequence, making it ideal for tests, simulations, and any scenario where repeatability matters.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Deterministic & reproducible sequences from identical seeds
|
|
8
|
+
- Flexible seeding: numbers, strings, arrays, hashes, Time objects, and more
|
|
9
|
+
- Floating point values in [0.0, 1.0) and integers within given ranges
|
|
10
|
+
- Hexadecimal string generation of arbitrary length
|
|
11
|
+
- API surface broadly compatible with Ruby's built-in `Random`
|
|
12
|
+
|
|
13
|
+
## String generation methods (alphabetic / alphanumeric / hex)
|
|
14
|
+
|
|
15
|
+
This section documents the specs, boundary conditions, and determinism guarantees for the string helpers.
|
|
16
|
+
|
|
17
|
+
### Common rules
|
|
18
|
+
|
|
19
|
+
- Covered methods: `generator.hex(length)`, `generator.alphabetic(length)`, `generator.alphanumeric(length)`
|
|
20
|
+
- Argument `length` MUST be an Integer >= 0.
|
|
21
|
+
- Negative or non-integer (e.g. Float) raises: `ArgumentError: "Length must be a non-negative integer"`.
|
|
22
|
+
- When `length == 0` an empty string (`""`) is returned.
|
|
23
|
+
- The returned string length always equals `length`.
|
|
24
|
+
- Output is deterministic w.r.t. the seed AND the exact call order of all generator methods.
|
|
25
|
+
- Same seed + identical sequence of method calls + identical `length` values => identical sequence of outputs.
|
|
26
|
+
- Different seeds produce statistically different outputs.
|
|
27
|
+
- Implementation wraps Ruby's `Random`. Characters are produced in fixed-size chunks by drawing a uniformly distributed integer and converting it to a mixed‑radix representation. Chunk sizing is part of the public deterministic algorithm; changing it is a breaking (MAJOR) change (see Determinism Policy below).
|
|
28
|
+
|
|
29
|
+
### Per-method specifics
|
|
30
|
+
|
|
31
|
+
| Method | Character set | Notes |
|
|
32
|
+
| ---------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
33
|
+
| `hex(length)` | `0-9a-f` (16 chars, lowercase) | Uses 32‑bit integers -> 8 hex chars at a time; remainder (1–7) from another 32‑bit block prefix. Uniform over all hex strings of the requested length. |
|
|
34
|
+
| `alphabetic(length)` | `A-Z` (26) + `a-z` (26) = 52 | 3-char chunks (since 52^3 < 2^32) plus remainder (1–2 chars). Uniform. |
|
|
35
|
+
| `alphanumeric(length)` | `A-Z` (26) + `a-z` (26) + `0-9` (10) = 62 | 5-char chunks (62^5 < 2^32) plus remainder (1–4 chars). Uniform. |
|
|
36
|
+
|
|
37
|
+
### Uniformity
|
|
38
|
+
|
|
39
|
+
Each chunk uses `Random#rand(base^k)` for an integer in `0...(base^k)` which is then expanded via repeated mod/div to `k` characters. This yields a uniform distribution over all `base^k` length-`k` strings. Remainder segments use the same approach. Thus (subject to the statistical quality of Ruby's underlying PRNG) each character position is unbiased and independent across chunks.
|
|
40
|
+
|
|
41
|
+
### Performance / limits
|
|
42
|
+
|
|
43
|
+
- Time complexity: O(length). Memory: O(length) for the resulting string.
|
|
44
|
+
- Very large values (millions of characters) imply higher allocation cost; consider generating in smaller segments if required.
|
|
45
|
+
|
|
46
|
+
### Determinism Policy
|
|
47
|
+
|
|
48
|
+
As stated in Versioning: the mapping `(seed, call order) -> output sequence` is a public contract.
|
|
49
|
+
|
|
50
|
+
- PATCH / MINOR: Existing deterministic sequences are preserved (unless fixing clearly incorrect behavior as per docs).
|
|
51
|
+
- MAJOR: We may alter internal chunk sizing / conversion strategy. Any such change will be called out in the CHANGELOG.
|
|
52
|
+
- The seed normalization algorithm (FNV‑1a based canonicalization) is also part of the deterministic surface; modifying it (outside critical bug fixes) is MAJOR.
|
|
53
|
+
|
|
54
|
+
### Exceptions (current)
|
|
55
|
+
|
|
56
|
+
| Condition | Exception |
|
|
57
|
+
| ----------------------- | --------------- |
|
|
58
|
+
| `length < 0` | `ArgumentError` |
|
|
59
|
+
| `length` not an Integer | `ArgumentError` |
|
|
60
|
+
|
|
61
|
+
### Examples
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
g = PseudoRandom.new("seed")
|
|
65
|
+
g.hex(10) # => 10 hex chars (0-9a-f)
|
|
66
|
+
g.alphabetic(12) # => 12 alphabetic chars (A-Za-z)
|
|
67
|
+
g.alphanumeric(8) # => 8 alphanumeric chars (A-Za-z0-9)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
See `CHANGELOG.md` for detailed release notes.
|
|
71
|
+
|
|
72
|
+
## ⚠️ Security / Cryptographic Use Disclaimer
|
|
73
|
+
|
|
74
|
+
The random values produced by this library prioritize determinism and reproducibility. They are NOT cryptographically secure. Do NOT use this library for any of the following:
|
|
75
|
+
|
|
76
|
+
- Password or passphrase generation
|
|
77
|
+
- API keys, access tokens, session IDs, CSRF tokens
|
|
78
|
+
- Cryptographic keys, IVs, nonces, salts
|
|
79
|
+
- Lotteries, drawings, or any fairness-critical public process exposed to adversaries
|
|
80
|
+
|
|
81
|
+
For those purposes use Ruby's standard `SecureRandom`, or a cryptographically secure source via OpenSSL / libsodium. Always use a CSPRNG for any security-sensitive or fairness‑critical context (passwords, keys, tokens, lotteries, audits, public selections, etc.).
|
|
82
|
+
|
|
83
|
+
Example (when you need secure randomness):
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
require 'securerandom'
|
|
87
|
+
token = SecureRandom.hex(32) # 64 hex characters
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use PseudoRandom only in contexts where determinism is valuable: tests, simulations, reproducible data generation, behavior snapshots with fixed seeds, etc.
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
Add this line to your Gemfile:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
gem 'pseudo_random'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then execute:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bundle install
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Or install directly:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
gem install pseudo_random
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Usage
|
|
113
|
+
|
|
114
|
+
### Basic usage
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
require 'pseudo_random'
|
|
118
|
+
|
|
119
|
+
# Create a generator with seed 42
|
|
120
|
+
generator = PseudoRandom.new(42)
|
|
121
|
+
|
|
122
|
+
# Float in [0.0, 1.0)
|
|
123
|
+
random_float = generator.rand
|
|
124
|
+
puts random_float # => 0.6394267984578837
|
|
125
|
+
|
|
126
|
+
# Integer in [0, 9]
|
|
127
|
+
random_int = generator.rand(10)
|
|
128
|
+
puts random_int # => 6
|
|
129
|
+
|
|
130
|
+
# Float in [0.0, 10.0)
|
|
131
|
+
random_float_range = generator.rand(10.0)
|
|
132
|
+
puts random_float_range # => 9.66814512009282
|
|
133
|
+
|
|
134
|
+
# Integer in [1, 100]
|
|
135
|
+
random_range = generator.rand(1..100)
|
|
136
|
+
puts random_range # => 64
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Convenience one-off method
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# One-off random number (legacy convenience)
|
|
143
|
+
result = PseudoRandom.rand(42)
|
|
144
|
+
puts result # => 0.6394267984578837
|
|
145
|
+
|
|
146
|
+
# Create a new generator explicitly
|
|
147
|
+
generator = PseudoRandom.new(42)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Diverse seed types
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# String seed
|
|
154
|
+
generator1 = PseudoRandom.new("hello")
|
|
155
|
+
puts generator1.rand # => 0.1915194503788923
|
|
156
|
+
|
|
157
|
+
# Array seed
|
|
158
|
+
generator2 = PseudoRandom.new([1, 2, 3])
|
|
159
|
+
puts generator2.rand # => 0.04548605918364251
|
|
160
|
+
|
|
161
|
+
# Hash seed
|
|
162
|
+
generator3 = PseudoRandom.new({ name: "John", age: 30 })
|
|
163
|
+
puts generator3.rand # => 0.7550896311312906
|
|
164
|
+
|
|
165
|
+
# Time seed
|
|
166
|
+
generator4 = PseudoRandom.new(Time.new(2023, 1, 1))
|
|
167
|
+
puts generator4.rand # => 0.4320558086698993
|
|
168
|
+
|
|
169
|
+
# Omitted seed (uses hash of nil)
|
|
170
|
+
generator5 = PseudoRandom.new
|
|
171
|
+
puts generator5.rand # => 0.8501480898450888
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Hex string generation
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
generator = PseudoRandom.new("secret")
|
|
178
|
+
|
|
179
|
+
# 8 hex characters
|
|
180
|
+
hex_string = generator.hex(8)
|
|
181
|
+
puts hex_string # => "a1b2c3d4"
|
|
182
|
+
|
|
183
|
+
# 10 hex characters
|
|
184
|
+
hex_string_10 = generator.hex(10)
|
|
185
|
+
puts hex_string_10 # => "a50ee918e5"
|
|
186
|
+
|
|
187
|
+
# 16 hex characters
|
|
188
|
+
long_hex = generator.hex(16)
|
|
189
|
+
puts long_hex # => "a1b2c3d4e5f67890"
|
|
190
|
+
|
|
191
|
+
# Empty string (length 0)
|
|
192
|
+
empty_hex = generator.hex(0)
|
|
193
|
+
puts empty_hex # => ""
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Reproducibility demonstration
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
# Two generators with the same seed
|
|
200
|
+
gen1 = PseudoRandom.new("test")
|
|
201
|
+
gen2 = PseudoRandom.new("test")
|
|
202
|
+
|
|
203
|
+
# Produces identical sequences
|
|
204
|
+
5.times do
|
|
205
|
+
puts "gen1: #{gen1.rand}, gen2: #{gen2.rand}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Example output:
|
|
209
|
+
# gen1: 0.5985762380674765, gen2: 0.5985762380674765
|
|
210
|
+
# gen1: 0.8325673044064309, gen2: 0.8325673044064309
|
|
211
|
+
# gen1: 0.24136065771243595, gen2: 0.24136065771243595
|
|
212
|
+
# gen1: 0.7392418174919607, gen2: 0.7392418174919607
|
|
213
|
+
# gen1: 0.9853406830436152, gen2: 0.9853406830436152
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Practical examples
|
|
217
|
+
|
|
218
|
+
#### Generating test data
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
# Consistent user test data
|
|
222
|
+
def generate_test_user(seed)
|
|
223
|
+
generator = PseudoRandom.new(seed)
|
|
224
|
+
|
|
225
|
+
{
|
|
226
|
+
id: generator.rand(1_000_000),
|
|
227
|
+
name: "User#{generator.hex(6)}",
|
|
228
|
+
score: generator.rand(100),
|
|
229
|
+
active: generator.rand(2) == 1
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
user1 = generate_test_user("user1")
|
|
234
|
+
user2 = generate_test_user("user1") # Same data
|
|
235
|
+
puts user1 == user2 # => true
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Simulation
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# Dice roll simulation
|
|
242
|
+
def simulate_dice_rolls(seed, count)
|
|
243
|
+
generator = PseudoRandom.new(seed)
|
|
244
|
+
results = Array.new(6, 0)
|
|
245
|
+
|
|
246
|
+
count.times do
|
|
247
|
+
roll = generator.rand(1..6)
|
|
248
|
+
results[roll - 1] += 1
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
results
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Identical results with identical seed
|
|
255
|
+
results1 = simulate_dice_rolls("dice_sim", 1000)
|
|
256
|
+
results2 = simulate_dice_rolls("dice_sim", 1000)
|
|
257
|
+
puts results1 == results2 # => true
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run `rake test` to run the test suite. You can also run `bin/console` for an interactive prompt to experiment.
|
|
263
|
+
|
|
264
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version: update the version in `version.rb`, then run `bundle exec rake release` (this creates a git tag, pushes commits and the tag, and publishes the `.gem` to [rubygems.org](https://rubygems.org)).
|
|
265
|
+
|
|
266
|
+
## Versioning
|
|
267
|
+
|
|
268
|
+
This project follows [Semantic Versioning 2.0.0](https://semver.org/).
|
|
269
|
+
|
|
270
|
+
Version numbers use the format MAJOR.MINOR.PATCH (e.g. `1.2.3`).
|
|
271
|
+
|
|
272
|
+
- MAJOR: Incremented for any backward-incompatible change to the public API. A change is considered breaking if it alters method names, argument semantics, return types, raises new errors in previously valid use, or changes the deterministic output sequence for the same seed in a way not explicitly documented as a bug fix.
|
|
273
|
+
- MINOR: Backward-compatible feature additions or expansions. May introduce new methods or optional arguments. Deterministic sequences for existing seeds remain unchanged (except where a PATCH-level bug fix applies).
|
|
274
|
+
- PATCH: Backward-compatible bug fixes and internal improvements that do not modify the documented behavior or output streams for existing seeds, unless the prior output was clearly incorrect per documentation (in which case the CHANGELOG will call it out explicitly).
|
|
275
|
+
|
|
276
|
+
Deprecations: A feature marked as deprecated will remain available for at least one MINOR release before removal in the next MAJOR. Deprecations are announced in the CHANGELOG under an "Deprecated" heading.
|
|
277
|
+
|
|
278
|
+
Deterministic Output Contract: The algorithm's mapping from (seed, call order) to values is part of the observable API. Altering it counts as a breaking change unless correcting a documented bug.
|
|
279
|
+
|
|
280
|
+
Pre-release tags (e.g. `1.1.0-alpha.1`) may be used for experimentation; they do not guarantee output stability until the final release.
|
|
281
|
+
|
|
282
|
+
The current version is defined in `lib/pseudo_random/version.rb` (`PseudoRandom::VERSION`).
|
|
283
|
+
|
|
284
|
+
## Contributing
|
|
285
|
+
|
|
286
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/aYosukeMakita/pseudo_random.
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pseudo_random/version'
|
|
4
|
+
|
|
5
|
+
module PseudoRandom
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Internal seed canonicalization & hashing (FNV-1a 64-bit, pure Ruby)
|
|
9
|
+
module Seed
|
|
10
|
+
FNV_OFFSET = 0xcbf29ce484222325
|
|
11
|
+
FNV_PRIME = 0x100000001b3
|
|
12
|
+
MASK64 = 0xffff_ffff_ffff_ffff
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Public: Convert arbitrary Ruby object to a deterministic 31-bit Integer for Random.new
|
|
17
|
+
def to_seed_int(obj)
|
|
18
|
+
h = FNV_OFFSET
|
|
19
|
+
canonical_each_byte(obj) do |byte|
|
|
20
|
+
h ^= byte
|
|
21
|
+
h = (h * FNV_PRIME) & MASK64
|
|
22
|
+
end
|
|
23
|
+
s = h ^ (h >> 32)
|
|
24
|
+
s & 0x7fff_ffff
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Depth-first canonical serialization streamed as bytes
|
|
28
|
+
def canonical_each_byte(obj, &blk)
|
|
29
|
+
case obj
|
|
30
|
+
when NilClass
|
|
31
|
+
yield 'n'.ord
|
|
32
|
+
when TrueClass
|
|
33
|
+
yield 't'.ord
|
|
34
|
+
when FalseClass
|
|
35
|
+
yield 'f'.ord
|
|
36
|
+
when Integer
|
|
37
|
+
yield 'i'.ord
|
|
38
|
+
encode_varint(zigzag(obj), &blk)
|
|
39
|
+
when Float
|
|
40
|
+
yield 'd'.ord
|
|
41
|
+
[obj].pack('G').each_byte(&blk) # big-endian IEEE 754
|
|
42
|
+
when String
|
|
43
|
+
str = obj.encode(Encoding::UTF_8)
|
|
44
|
+
yield 's'.ord
|
|
45
|
+
encode_varint(str.bytesize, &blk)
|
|
46
|
+
str.each_byte(&blk)
|
|
47
|
+
when Symbol
|
|
48
|
+
str = obj.to_s.encode(Encoding::UTF_8)
|
|
49
|
+
yield 'y'.ord
|
|
50
|
+
encode_varint(str.bytesize, &blk)
|
|
51
|
+
str.each_byte(&blk)
|
|
52
|
+
when Array
|
|
53
|
+
yield 'a'.ord
|
|
54
|
+
encode_varint(obj.length, &blk)
|
|
55
|
+
obj.each { |e| canonical_each_byte(e, &blk) }
|
|
56
|
+
when Hash
|
|
57
|
+
yield 'h'.ord
|
|
58
|
+
encode_varint(obj.length, &blk)
|
|
59
|
+
# Canonical order by key string representation to avoid insertion order dependence
|
|
60
|
+
obj.keys.map(&:to_s).sort.each do |ks|
|
|
61
|
+
canonical_each_byte(ks, &blk)
|
|
62
|
+
original_key = if obj.key?(ks)
|
|
63
|
+
ks
|
|
64
|
+
elsif obj.key?(ks.to_sym)
|
|
65
|
+
ks.to_sym
|
|
66
|
+
else
|
|
67
|
+
# Fallback (should not usually happen)
|
|
68
|
+
obj.keys.find { |k| k.to_s == ks }
|
|
69
|
+
end
|
|
70
|
+
canonical_each_byte(obj[original_key], &blk)
|
|
71
|
+
end
|
|
72
|
+
when Time
|
|
73
|
+
yield 'T'.ord
|
|
74
|
+
encode_varint(obj.to_i, &blk)
|
|
75
|
+
encode_varint(obj.nsec, &blk)
|
|
76
|
+
else
|
|
77
|
+
# Fallback: class name + ':' + to_s (could cause collisions if to_s not stable)
|
|
78
|
+
rep = "#{obj.class.name}:#{obj}"
|
|
79
|
+
rep = rep.encode(Encoding::UTF_8)
|
|
80
|
+
yield 'o'.ord
|
|
81
|
+
encode_varint(rep.bytesize, &blk)
|
|
82
|
+
rep.each_byte(&blk)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ZigZag encode signed -> unsigned integer
|
|
87
|
+
def zigzag(num)
|
|
88
|
+
num >= 0 ? (num << 1) : ((-num << 1) - 1)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Varint (7-bit continuation) encoding
|
|
92
|
+
def encode_varint(num)
|
|
93
|
+
raise ArgumentError, 'negative varint' if num < 0
|
|
94
|
+
|
|
95
|
+
loop do
|
|
96
|
+
byte = num & 0x7f
|
|
97
|
+
num >>= 7
|
|
98
|
+
if num.zero?
|
|
99
|
+
yield byte
|
|
100
|
+
break
|
|
101
|
+
else
|
|
102
|
+
yield(byte | 0x80)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class Generator
|
|
109
|
+
# Character set for alphabetic generation: A-Z (26) + a-z (26) = 52 characters
|
|
110
|
+
ALPHABETIC_CHARS = ('A'..'Z').to_a + ('a'..'z').to_a
|
|
111
|
+
# Character set for alphanumeric generation: A-Z (26) + a-z (26) + 0-9 (10) = 62 characters
|
|
112
|
+
ALPHANUMERIC_CHARS = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a
|
|
113
|
+
|
|
114
|
+
def initialize(seed = nil)
|
|
115
|
+
@random = Random.new(normalize_seed(seed))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Generates the next pseudo-random number in the sequence
|
|
119
|
+
# If max is provided, returns a value between 0 and max-1 (or 0.0 to max for floats)
|
|
120
|
+
# If a Range is provided, returns a value within that range
|
|
121
|
+
# If no arguments, returns a float between 0.0 and 1.0 (like Ruby's Kernel.#rand)
|
|
122
|
+
def rand(max = nil)
|
|
123
|
+
if max.nil?
|
|
124
|
+
@random.rand # Returns float between 0.0 and 1.0
|
|
125
|
+
else
|
|
126
|
+
@random.rand(max)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Generates a hexadecimal string with the specified number of characters
|
|
131
|
+
# @param length [Integer] the number of hexadecimal characters to generate (must be >= 0)
|
|
132
|
+
# @return [String] a hexadecimal string with lowercase a-f
|
|
133
|
+
def hex(length)
|
|
134
|
+
raise ArgumentError, 'Length must be a non-negative integer' unless length.is_a?(Integer) && length >= 0
|
|
135
|
+
|
|
136
|
+
return '' if length == 0
|
|
137
|
+
|
|
138
|
+
result = ''
|
|
139
|
+
remaining = length
|
|
140
|
+
|
|
141
|
+
# Process 8 characters at a time for efficiency (32-bit random number = 8 hex chars)
|
|
142
|
+
while remaining >= 8
|
|
143
|
+
# Generate a 32-bit random number and convert to 8-character hex string
|
|
144
|
+
random_value = @random.rand(2**32)
|
|
145
|
+
hex_chunk = format('%08x', random_value)
|
|
146
|
+
result += hex_chunk
|
|
147
|
+
remaining -= 8
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Process remaining characters (1-7) by generating 8 chars and taking what we need
|
|
151
|
+
if remaining > 0
|
|
152
|
+
random_value = @random.rand(2**32)
|
|
153
|
+
hex_chunk = format('%08x', random_value)
|
|
154
|
+
result += hex_chunk[0, remaining] # Take only the first 'remaining' characters
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
result
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Generates an alphabetic string with the specified number of characters
|
|
161
|
+
# @param length [Integer] the number of alphabetic characters to generate (must be >= 0)
|
|
162
|
+
# @return [String] a string containing uppercase letters (A-Z) and lowercase letters (a-z)
|
|
163
|
+
def alphabetic(length)
|
|
164
|
+
raise ArgumentError, 'Length must be a non-negative integer' unless length.is_a?(Integer) && length >= 0
|
|
165
|
+
|
|
166
|
+
return '' if length == 0
|
|
167
|
+
|
|
168
|
+
result = ''
|
|
169
|
+
remaining = length
|
|
170
|
+
|
|
171
|
+
# Process multiple characters at once for efficiency
|
|
172
|
+
# We can generate about 3 characters from a 32-bit random number (52^3 = 140,608 < 2^32)
|
|
173
|
+
chunk_size = 3
|
|
174
|
+
|
|
175
|
+
while remaining >= chunk_size
|
|
176
|
+
# Generate a random number and convert to base-52 representation
|
|
177
|
+
random_value = @random.rand(52**chunk_size)
|
|
178
|
+
chunk = ''
|
|
179
|
+
|
|
180
|
+
chunk_size.times do
|
|
181
|
+
chunk = ALPHABETIC_CHARS[random_value % 52] + chunk
|
|
182
|
+
random_value /= 52
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
result += chunk
|
|
186
|
+
remaining -= chunk_size
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Process remaining characters (1-2)
|
|
190
|
+
if remaining > 0
|
|
191
|
+
random_value = @random.rand(52**remaining)
|
|
192
|
+
chunk = ''
|
|
193
|
+
|
|
194
|
+
remaining.times do
|
|
195
|
+
chunk = ALPHABETIC_CHARS[random_value % 52] + chunk
|
|
196
|
+
random_value /= 52
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
result += chunk
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
result
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Generates an alphanumeric string with the specified number of characters
|
|
206
|
+
# @param length [Integer] the number of alphanumeric characters to generate (must be >= 0)
|
|
207
|
+
# @return [String] a string containing uppercase letters (A-Z), lowercase letters (a-z), and digits (0-9)
|
|
208
|
+
def alphanumeric(length)
|
|
209
|
+
raise ArgumentError, 'Length must be a non-negative integer' unless length.is_a?(Integer) && length >= 0
|
|
210
|
+
|
|
211
|
+
return '' if length == 0
|
|
212
|
+
|
|
213
|
+
result = ''
|
|
214
|
+
remaining = length
|
|
215
|
+
|
|
216
|
+
# Process multiple characters at once for efficiency
|
|
217
|
+
# We can generate about 5 characters from a 32-bit random number (62^5 = 916,132,832 < 2^32)
|
|
218
|
+
chunk_size = 5
|
|
219
|
+
|
|
220
|
+
while remaining >= chunk_size
|
|
221
|
+
# Generate a random number and convert to base-62 representation
|
|
222
|
+
random_value = @random.rand(62**chunk_size)
|
|
223
|
+
chunk = ''
|
|
224
|
+
|
|
225
|
+
chunk_size.times do
|
|
226
|
+
chunk = ALPHANUMERIC_CHARS[random_value % 62] + chunk
|
|
227
|
+
random_value /= 62
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
result += chunk
|
|
231
|
+
remaining -= chunk_size
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Process remaining characters (1-4)
|
|
235
|
+
if remaining > 0
|
|
236
|
+
random_value = @random.rand(62**remaining)
|
|
237
|
+
chunk = ''
|
|
238
|
+
|
|
239
|
+
remaining.times do
|
|
240
|
+
chunk = ALPHANUMERIC_CHARS[random_value % 62] + chunk
|
|
241
|
+
random_value /= 62
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
result += chunk
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
result
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
# Deterministic, process-independent reduction of arbitrary object to Integer seed.
|
|
253
|
+
def normalize_seed(seed)
|
|
254
|
+
Seed.to_seed_int(seed)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Creates a new generator with the given seed
|
|
259
|
+
def self.new(seed = nil)
|
|
260
|
+
Generator.new(seed)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Generates a single pseudo-random number based on the given seed (backward compatibility)
|
|
264
|
+
def self.rand(seed)
|
|
265
|
+
generator = new(seed)
|
|
266
|
+
generator.rand
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module PseudoRandom
|
|
2
|
+
VERSION: String
|
|
3
|
+
# See the writing guide of rbs: https://github.com/ruby/rbs#guides
|
|
4
|
+
|
|
5
|
+
class Generator
|
|
6
|
+
ALPHABETIC_CHARS: Array[String]
|
|
7
|
+
ALPHANUMERIC_CHARS: Array[String]
|
|
8
|
+
|
|
9
|
+
def initialize: (?untyped seed) -> void
|
|
10
|
+
|
|
11
|
+
def rand: () -> Float
|
|
12
|
+
| (Integer max) -> Integer
|
|
13
|
+
| (Float max) -> Float
|
|
14
|
+
| (Range[Integer, Integer] range) -> Integer
|
|
15
|
+
| (Range[Float, Float] range) -> Float
|
|
16
|
+
|
|
17
|
+
def hex: (Integer length) -> String
|
|
18
|
+
def alphabetic: (Integer length) -> String
|
|
19
|
+
def alphanumeric: (Integer length) -> String
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.new: (?untyped seed) -> Generator
|
|
23
|
+
def self.rand: (untyped seed) -> Float
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pseudo_random
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- aYosukeMakita
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Generates reproducible (deterministic) pseudo-random numbers and strings
|
|
13
|
+
(hex/alphabetic/alphanumeric) from arbitrary Ruby object seeds. Not cryptographically
|
|
14
|
+
secure.
|
|
15
|
+
email:
|
|
16
|
+
- yosuke.makita@access-company.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- ".rubocop.yml"
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE.txt
|
|
24
|
+
- README.md
|
|
25
|
+
- Rakefile
|
|
26
|
+
- lib/pseudo_random.rb
|
|
27
|
+
- lib/pseudo_random/version.rb
|
|
28
|
+
- sig/pseudo_random.rbs
|
|
29
|
+
homepage: https://github.com/aYosukeMakita/pseudo_random
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata:
|
|
33
|
+
homepage_uri: https://github.com/aYosukeMakita/pseudo_random
|
|
34
|
+
source_code_uri: https://github.com/aYosukeMakita/pseudo_random
|
|
35
|
+
changelog_uri: https://github.com/aYosukeMakita/pseudo_random/blob/main/CHANGELOG.md
|
|
36
|
+
rubygems_mfa_required: 'true'
|
|
37
|
+
issue_tracker_uri: https://github.com/aYosukeMakita/pseudo_random/issues
|
|
38
|
+
documentation_uri: https://www.rubydoc.info/gems/pseudo_random
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 3.1.0
|
|
47
|
+
- - "<"
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '4.0'
|
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
requirements: []
|
|
56
|
+
rubygems_version: 3.6.9
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: Deterministic pseudo-random generator for numbers and strings.
|
|
59
|
+
test_files: []
|