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 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'minitest/test_task'
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PseudoRandom
4
+ VERSION = '1.0.0'
5
+ end
@@ -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: []