use crate::rng::RngSource;
const FLAG_CONSONANT: u8 = 0b0001;
const FLAG_VOWEL: u8 = 0b0010;
const FLAG_DIPTHONG: u8 = 0b0100;
const FLAG_NOT_FIRST: u8 = 0b1000;
struct Phoneme {
bytes: &'static [u8],
flags: u8,
}
const PHONEMES: &[Phoneme] = &[
Phoneme {
bytes: b"a",
flags: FLAG_VOWEL,
},
Phoneme {
bytes: b"ae",
flags: FLAG_VOWEL | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"ah",
flags: FLAG_VOWEL | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"ai",
flags: FLAG_VOWEL | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"b",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"c",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"ch",
flags: FLAG_CONSONANT | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"d",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"e",
flags: FLAG_VOWEL,
},
Phoneme {
bytes: b"ee",
flags: FLAG_VOWEL | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"ei",
flags: FLAG_VOWEL | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"f",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"g",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"gh",
flags: FLAG_CONSONANT | FLAG_DIPTHONG | FLAG_NOT_FIRST,
},
Phoneme {
bytes: b"h",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"i",
flags: FLAG_VOWEL,
},
Phoneme {
bytes: b"ie",
flags: FLAG_VOWEL | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"j",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"k",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"l",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"m",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"n",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"ng",
flags: FLAG_CONSONANT | FLAG_DIPTHONG | FLAG_NOT_FIRST,
},
Phoneme {
bytes: b"o",
flags: FLAG_VOWEL,
},
Phoneme {
bytes: b"oh",
flags: FLAG_VOWEL | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"oo",
flags: FLAG_VOWEL | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"p",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"ph",
flags: FLAG_CONSONANT | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"qu",
flags: FLAG_CONSONANT | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"r",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"s",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"sh",
flags: FLAG_CONSONANT | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"t",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"th",
flags: FLAG_CONSONANT | FLAG_DIPTHONG,
},
Phoneme {
bytes: b"u",
flags: FLAG_VOWEL,
},
Phoneme {
bytes: b"v",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"w",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"x",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"y",
flags: FLAG_CONSONANT,
},
Phoneme {
bytes: b"z",
flags: FLAG_CONSONANT,
},
];
pub fn generate<R: RngSource + ?Sized>(
rng: &mut R,
length: usize,
capitalize: bool,
numerals: bool,
) -> String {
if length == 0 {
return String::new();
}
let effective_caps = capitalize && length > 2;
let effective_digits = numerals && length > 1;
let mut out = Vec::with_capacity(length);
let mut should_be: u8 = if rng.gen_range(2) == 0 {
FLAG_VOWEL
} else {
FLAG_CONSONANT
};
let mut prev_flags: u8 = 0;
let mut first = true;
while out.len() < length {
let idx = rng.gen_range(PHONEMES.len() as u32) as usize;
let p = &PHONEMES[idx];
if (p.flags & should_be) == 0 {
continue;
}
if first && (p.flags & FLAG_NOT_FIRST) != 0 {
continue;
}
if (prev_flags & FLAG_VOWEL) != 0
&& (p.flags & FLAG_VOWEL) != 0
&& (p.flags & FLAG_DIPTHONG) != 0
{
continue;
}
if out.len() + p.bytes.len() > length {
continue;
}
let mut bytes_to_push: Vec<u8> = p.bytes.to_vec();
if effective_caps && (first || (p.flags & FLAG_CONSONANT) != 0) && rng.gen_range(10) < 2 {
bytes_to_push[0] = bytes_to_push[0].to_ascii_uppercase();
}
out.extend_from_slice(&bytes_to_push);
if (should_be & FLAG_CONSONANT) != 0 {
should_be = FLAG_VOWEL;
} else {
should_be = if rng.gen_range(10) < 3 && !is_dipthong(p.flags) {
FLAG_VOWEL
} else {
FLAG_CONSONANT
};
}
prev_flags = p.flags;
first = false;
if effective_digits && out.len() < length && rng.gen_range(10) < 3 {
let digit = b'0' + (rng.gen_range(10) as u8);
out.push(digit);
first = true;
prev_flags = 0;
should_be = if rng.gen_range(2) == 0 {
FLAG_VOWEL
} else {
FLAG_CONSONANT
};
}
}
String::from_utf8(out).expect("phoneme output is ASCII")
}
fn is_dipthong(flags: u8) -> bool {
(flags & FLAG_DIPTHONG) != 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rng::OsRngSource;
#[test]
fn generate_length_zero_is_empty() {
let mut rng = OsRngSource::new();
assert_eq!(generate(&mut rng, 0, true, true), "");
}
#[test]
fn generate_default_length_8_is_8_chars() {
let mut rng = OsRngSource::new();
for _ in 0..50 {
let s = generate(&mut rng, 8, true, true);
assert_eq!(s.len(), 8, "expected 8 chars, got {s:?}");
assert!(s.is_ascii());
}
}
#[test]
fn generate_length_12_is_12_chars() {
let mut rng = OsRngSource::new();
for _ in 0..50 {
let s = generate(&mut rng, 12, true, true);
assert_eq!(s.len(), 12);
}
}
#[test]
fn generate_consecutive_outputs_differ() {
let mut rng = OsRngSource::new();
let mut seen = std::collections::HashSet::new();
for _ in 0..20 {
seen.insert(generate(&mut rng, 12, true, true));
}
assert!(
seen.len() > 1,
"10+ consecutive calls should produce distinct outputs"
);
}
#[test]
fn generate_with_no_caps_no_digits_is_pure_lowercase() {
let mut rng = OsRngSource::new();
let s = generate(&mut rng, 16, false, false);
assert!(
s.bytes().all(|b| b.is_ascii_lowercase()),
"no caps, no digits → pure lowercase, got {s:?}"
);
}
#[test]
fn generate_length_2_suppresses_caps_silently() {
let mut rng = OsRngSource::new();
for _ in 0..50 {
let s = generate(&mut rng, 2, true, true);
assert_eq!(s.len(), 2);
assert!(
s.bytes().all(|b| !b.is_ascii_uppercase()),
"length 2 must not capitalize, got {s:?}"
);
}
}
#[test]
fn generate_length_1_suppresses_digits_silently() {
let mut rng = OsRngSource::new();
for _ in 0..50 {
let s = generate(&mut rng, 1, false, true);
assert_eq!(s.len(), 1);
assert!(
s.bytes().next().unwrap().is_ascii_alphabetic(),
"length 1 must not be a digit, got {s:?}"
);
}
}
#[test]
fn deterministic_with_seeded_rng() {
use crate::rng::SeededSource;
let seed = [99u8; 32];
let mut a = SeededSource::from_seed(seed);
let mut b = SeededSource::from_seed(seed);
for _ in 0..10 {
assert_eq!(
generate(&mut a, 12, true, true),
generate(&mut b, 12, true, true)
);
}
}
}