readable_code_core/
lib.rs1use std::time::{SystemTime, UNIX_EPOCH};
9
10pub trait RandomSource {
15 fn gen_below(&mut self, max_exclusive: u32) -> u32;
17}
18
19pub struct SplitMix64 {
26 state: u64,
27}
28
29impl SplitMix64 {
30 pub fn new(seed: u64) -> Self {
32 Self { state: seed }
33 }
34
35 pub fn from_entropy() -> Self {
37 let nanos = SystemTime::now()
38 .duration_since(UNIX_EPOCH)
39 .map(|d| d.as_nanos() as u64)
40 .unwrap_or(0x9E37_79B9_7F4A_7C15);
41 Self::new(nanos ^ 0x9E37_79B9_7F4A_7C15)
42 }
43
44 pub fn next_u64(&mut self) -> u64 {
48 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
49 let mut z = self.state;
50 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
51 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
52 z ^ (z >> 31)
53 }
54}
55
56impl RandomSource for SplitMix64 {
57 fn gen_below(&mut self, max_exclusive: u32) -> u32 {
58 assert!(max_exclusive > 0, "max_exclusive must be positive");
59 let m = u64::from(max_exclusive);
60 let limit = (u64::MAX / m) * m;
62 loop {
63 let value = self.next_u64();
64 if value < limit {
65 return (value % m) as u32;
66 }
67 }
68 }
69}
70
71#[derive(Debug, Default, Clone, Copy)]
78pub struct OsRandom;
79
80impl OsRandom {
81 pub fn new() -> Self {
83 Self
84 }
85}
86
87impl RandomSource for OsRandom {
88 fn gen_below(&mut self, max_exclusive: u32) -> u32 {
89 assert!(max_exclusive > 0, "max_exclusive must be positive");
90 let m = u64::from(max_exclusive);
91 let limit = (u64::MAX / m) * m;
93 loop {
94 let mut buf = [0u8; 8];
95 getrandom::getrandom(&mut buf).expect("OS CSPRNG (getrandom) failed");
96 let value = u64::from_le_bytes(buf);
97 if value < limit {
98 return (value % m) as u32;
99 }
100 }
101 }
102}
103
104pub fn pick<'a, T, R: RandomSource>(items: &'a [T], rng: &mut R) -> &'a T {
108 assert!(!items.is_empty(), "cannot pick from an empty slice");
109 let index = rng.gen_below(items.len() as u32) as usize;
110 &items[index]
111}
112
113pub fn digits<R: RandomSource>(length: usize, rng: &mut R) -> String {
115 let mut out = String::with_capacity(length);
116 for _ in 0..length {
117 let digit = rng.gen_below(10) as u8;
118 out.push((b'0' + digit) as char);
119 }
120 out
121}
122
123pub struct CodeBuilder<R: RandomSource> {
126 parts: Vec<String>,
127 rng: R,
128}
129
130impl<R: RandomSource> CodeBuilder<R> {
131 pub fn new(rng: R) -> Self {
133 Self {
134 parts: Vec::new(),
135 rng,
136 }
137 }
138
139 pub fn add(mut self, value: impl Into<String>) -> Self {
141 self.parts.push(value.into());
142 self
143 }
144
145 pub fn add_with<F: FnOnce(&mut R) -> String>(mut self, f: F) -> Self {
150 let value = f(&mut self.rng);
151 self.parts.push(value);
152 self
153 }
154
155 pub fn dash(self) -> Self {
157 self.add("-")
158 }
159
160 pub fn digits(self, length: usize) -> Self {
162 self.add_with(|rng| digits(length, rng))
163 }
164
165 pub fn nums(self, length: usize) -> Self {
167 self.digits(length)
168 }
169
170 pub fn build(self) -> String {
172 self.parts.concat()
173 }
174}
175
176pub fn code<R: RandomSource>(rng: R) -> CodeBuilder<R> {
178 CodeBuilder::new(rng)
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 struct Fixed(u32);
187 impl RandomSource for Fixed {
188 fn gen_below(&mut self, max_exclusive: u32) -> u32 {
189 self.0 % max_exclusive
190 }
191 }
192
193 struct Seq {
195 values: Vec<u32>,
196 index: usize,
197 }
198 impl RandomSource for Seq {
199 fn gen_below(&mut self, max_exclusive: u32) -> u32 {
200 let value = self.values[self.index % self.values.len()];
201 self.index += 1;
202 value % max_exclusive
203 }
204 }
205
206 #[test]
207 fn digits_are_deterministic() {
208 assert_eq!(digits(4, &mut Fixed(7)), "7777");
209 assert_eq!(
210 digits(
211 4,
212 &mut Seq {
213 values: vec![1, 2, 3, 4],
214 index: 0
215 }
216 ),
217 "1234"
218 );
219 }
220
221 #[test]
222 fn dash_appends_single_hyphen() {
223 assert_eq!(code(Fixed(0)).add("ab").dash().add("cd").build(), "ab-cd");
224 }
225
226 #[test]
227 fn nums_is_alias_for_digits() {
228 assert_eq!(code(Fixed(5)).digits(3).build(), code(Fixed(5)).nums(3).build());
229 }
230
231 #[test]
232 fn composes_full_code() {
233 assert_eq!(code(Fixed(0)).add("teva").dash().digits(4).build(), "teva-0000");
234 }
235
236 #[test]
237 fn seeded_default_stays_in_range() {
238 let mut rng = SplitMix64::new(42);
239 for _ in 0..1000 {
240 assert!(rng.gen_below(10) < 10);
241 }
242 }
243
244 #[test]
245 fn os_random_stays_in_range() {
246 let mut rng = OsRandom::new();
247 for _ in 0..1000 {
248 assert!(rng.gen_below(10) < 10);
249 }
250 }
251
252 #[test]
253 fn digits_empty_for_zero_length() {
254 assert_eq!(digits(0, &mut Fixed(7)), "");
255 }
256
257 #[test]
258 fn digits_maps_every_index_to_its_decimal() {
259 assert_eq!(
260 digits(
261 10,
262 &mut Seq {
263 values: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
264 index: 0
265 }
266 ),
267 "0123456789"
268 );
269 }
270
271 #[test]
272 fn empty_builder_builds_empty_string() {
273 assert_eq!(code(Fixed(0)).build(), "");
274 }
275
276 #[test]
277 fn builder_preserves_empty_fragments() {
278 assert_eq!(code(Fixed(0)).add("a").add("").add("b").build(), "ab");
279 }
280
281 #[test]
282 fn builder_preserves_fragment_order() {
283 assert_eq!(
284 code(Fixed(0)).add("a").dash().add("b").dash().add("c").build(),
285 "a-b-c"
286 );
287 }
288
289 #[test]
290 fn pick_returns_singleton_element() {
291 assert_eq!(*pick(&["solo"], &mut Fixed(0)), "solo");
292 }
293
294 #[test]
295 fn pick_uses_index_from_source() {
296 assert_eq!(*pick(&["a", "b", "c", "d"], &mut Fixed(2)), "c");
297 }
298
299 #[test]
300 #[should_panic(expected = "cannot pick from an empty slice")]
301 fn pick_panics_on_empty_slice() {
302 let empty: [u8; 0] = [];
303 pick(&empty, &mut Fixed(0));
304 }
305
306 #[test]
307 #[should_panic(expected = "max_exclusive must be positive")]
308 fn splitmix_panics_on_zero_bound() {
309 SplitMix64::new(1).gen_below(0);
310 }
311
312 #[test]
313 #[should_panic(expected = "max_exclusive must be positive")]
314 fn os_random_panics_on_zero_bound() {
315 OsRandom::new().gen_below(0);
316 }
317
318 #[test]
319 fn gen_below_one_is_always_zero() {
320 let mut rng = SplitMix64::new(123);
321 for _ in 0..100 {
322 assert_eq!(rng.gen_below(1), 0);
323 }
324 }
325
326 #[test]
327 fn splitmix_is_reproducible_for_a_seed() {
328 let draw = |seed: u64| {
329 let mut rng = SplitMix64::new(seed);
330 (0..16).map(|_| rng.gen_below(1000)).collect::<Vec<_>>()
331 };
332 assert_eq!(draw(42), draw(42));
333 assert_ne!(draw(42), draw(43));
334 }
335
336 #[test]
337 fn splitmix_next_u64_is_deterministic() {
338 let mut a = SplitMix64::new(0);
339 let mut b = SplitMix64::new(0);
340 for _ in 0..8 {
341 assert_eq!(a.next_u64(), b.next_u64());
342 }
343 }
344
345 #[test]
346 fn gen_below_covers_full_small_range() {
347 let mut rng = SplitMix64::new(7);
348 let mut seen = [false; 4];
349 for _ in 0..2000 {
350 seen[rng.gen_below(4) as usize] = true;
351 }
352 assert!(seen.iter().all(|&hit| hit));
353 }
354}