use alloc::borrow::Cow;
use alloc::string::String;
use rand::seq::IndexedRandom;
use crate::{Generator, List, Lists, Namer};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Gender {
Masculine,
Feminine,
Neuter,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Noun<'a> {
pub word: &'a str,
pub gender: Gender,
}
impl<'a> Noun<'a> {
pub const fn new(word: &'a str, gender: Gender) -> Self {
Self { word, gender }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Adjective<'a> {
pub masculine: &'a str,
pub feminine: &'a str,
pub neuter: &'a str,
}
impl<'a> Adjective<'a> {
pub const fn new(masculine: &'a str, feminine: &'a str, neuter: &'a str) -> Self {
Self { masculine, feminine, neuter }
}
pub const fn agree(&self, gender: Gender) -> &'a str {
match gender {
Gender::Masculine => self.masculine,
Gender::Feminine => self.feminine,
Gender::Neuter => self.neuter,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Petnames<'a> {
pub adjectives: Cow<'a, [Adjective<'a>]>,
pub adverbs: Cow<'a, [&'a str]>,
pub nouns: Cow<'a, [Noun<'a>]>,
}
impl<'a> Petnames<'a> {
#[cfg(feature = "default-words")]
pub fn small() -> Self {
crate::luxembourgish!("words/luxembourgish")
}
pub fn retain<F>(&mut self, mut predicate: F)
where
F: FnMut(&str) -> bool,
{
self.adjectives.to_mut().retain(|adjective| {
predicate(adjective.masculine) && predicate(adjective.feminine) && predicate(adjective.neuter)
});
self.adverbs.to_mut().retain(|word| predicate(word));
self.nouns.to_mut().retain(|noun| predicate(noun.word));
}
pub fn cardinality(&self, words: u8) -> u128 {
Lists::new(words)
.map(|list| match list {
List::Adverb => self.adverbs.len() as u128,
List::Adjective => self.adjectives.len() as u128,
List::Noun => self.nouns.len() as u128,
})
.reduce(u128::saturating_mul)
.unwrap_or(0u128)
}
pub fn namer<'b>(&'b self, words: u8, separator: &'b str) -> Namer<'b, Self> {
Namer { generator: self, words, separator }
}
}
impl Generator for Petnames<'_> {
fn generate_into(&self, buf: &mut String, rng: &mut dyn rand::Rng, words: u8, separator: &str) {
let noun = self.nouns.choose(rng);
let gender = noun.map_or(Gender::Feminine, |noun| noun.gender);
let mut pending: Option<&str> = None;
for list in Lists::new(words) {
let word = match list {
List::Adverb => self.adverbs.choose(rng).copied(),
List::Adjective => self.adjectives.choose(rng).map(|adjective| adjective.agree(gender)),
List::Noun => noun.map(|noun| noun.word),
};
if let Some(word) = word {
if let Some(previous) = pending.replace(word) {
buf.push_str(elide_n(previous, word));
buf.push_str(separator);
}
}
}
if let Some(last) = pending {
buf.push_str(last);
}
}
}
fn elide_n<'a>(word: &'a str, next: &str) -> &'a str {
match next.chars().next() {
Some(next) if triggers_elision(next) => {
word.strip_suffix("nn").or_else(|| word.strip_suffix('n')).unwrap_or(word)
}
_ => word,
}
}
fn triggers_elision(next: char) -> bool {
let next = next.to_lowercase().next().unwrap_or(next);
let keeps_n = matches!(next, 'n' | 'd' | 't' | 'z' | 'h');
let is_vowel = matches!(
next,
'a' | 'e'
| 'i'
| 'o'
| 'u'
| 'é'
| 'è'
| 'ë'
| 'ä'
| 'ö'
| 'ü'
| 'á'
| 'à'
| 'â'
| 'ê'
| 'î'
| 'ô'
| 'û'
);
!(keeps_n || is_vowel)
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use alloc::vec;
use super::{elide_n, Adjective, Gender, Noun, Petnames};
fn sample() -> Petnames<'static> {
Petnames {
adjectives: Cow::Owned(vec![
Adjective::new("groussen", "grouss", "grousst"),
Adjective::new("klengen", "kleng", "klengt"),
]),
adverbs: Cow::Owned(vec!["ganz", "richteg"]),
nouns: Cow::Owned(vec![
Noun::new("mupp", Gender::Masculine),
Noun::new("kaz", Gender::Feminine),
Noun::new("haus", Gender::Neuter),
]),
}
}
#[test]
fn agree_selects_form_by_gender() {
let adjective = Adjective::new("groussen", "grouss", "grousst");
assert_eq!(adjective.agree(Gender::Masculine), "groussen");
assert_eq!(adjective.agree(Gender::Feminine), "grouss");
assert_eq!(adjective.agree(Gender::Neuter), "grousst");
}
#[test]
fn eifeler_regel_elides_n_only_before_triggering_sounds() {
assert_eq!(elide_n("groussen", "mupp"), "grousse");
assert_eq!(elide_n("groussen", "bam"), "grousse");
assert_eq!(elide_n("groussen", "Kaz"), "grousse"); assert_eq!(elide_n("groussen", "hond"), "groussen");
assert_eq!(elide_n("groussen", "donner"), "groussen");
assert_eq!(elide_n("groussen", "zocker"), "groussen");
assert_eq!(elide_n("groussen", "apel"), "groussen");
assert_eq!(elide_n("groussen", "éiweg"), "groussen");
assert_eq!(elide_n("grouss", "mupp"), "grouss");
assert_eq!(elide_n("dënn", "mupp"), "dë");
assert_eq!(elide_n("dënn", "hond"), "dënn");
}
#[cfg(feature = "default-rng")]
fn generate(lb: &Petnames, words: u8, seed: u64) -> alloc::vec::Vec<alloc::string::String> {
use rand::SeedableRng;
let namer = lb.namer(words, "-");
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
(0..50)
.map(|_| {
let mut buf = alloc::string::String::new();
namer.generate_into(&mut buf, &mut rng);
buf
})
.collect()
}
#[cfg(feature = "default-rng")]
#[test]
fn token_count_matches_words() {
let lb = sample();
for words in 1..=5u8 {
for name in generate(&lb, words, 1) {
assert_eq!(name.split('-').count(), words as usize, "name was {name:?}");
}
}
}
#[cfg(feature = "default-rng")]
#[test]
fn agreement_and_elision_are_applied() {
use rand::SeedableRng;
let masculine = Petnames {
adjectives: Cow::Owned(vec![Adjective::new("groussen", "grouss", "grousst")]),
adverbs: Cow::Owned(vec![]),
nouns: Cow::Owned(vec![Noun::new("mupp", Gender::Masculine)]),
};
let feminine =
Petnames { nouns: Cow::Owned(vec![Noun::new("kaz", Gender::Feminine)]), ..masculine.clone() };
let kept =
Petnames { nouns: Cow::Owned(vec![Noun::new("hond", Gender::Masculine)]), ..masculine.clone() };
let mut rng = rand::rngs::StdRng::seed_from_u64(1);
let render = |lb: &Petnames, rng: &mut rand::rngs::StdRng| {
let mut buf = alloc::string::String::new();
lb.namer(2, "-").generate_into(&mut buf, rng);
buf
};
assert_eq!(render(&masculine, &mut rng), "grousse-mupp");
assert_eq!(render(&feminine, &mut rng), "grouss-kaz");
assert_eq!(render(&kept, &mut rng), "groussen-hond");
}
#[cfg(feature = "default-rng")]
#[test]
fn deterministic_under_seed() {
let lb = sample();
assert_eq!(generate(&lb, 3, 42), generate(&lb, 3, 42));
}
#[test]
fn cardinality_counts_combinations() {
let lb = sample(); assert_eq!(lb.cardinality(1), 3); assert_eq!(lb.cardinality(2), 6); assert_eq!(lb.cardinality(3), 12); assert_eq!(lb.cardinality(0), 0);
}
#[cfg(feature = "default-words")]
#[test]
fn small_parses_genders_and_three_forms() {
let lb = Petnames::small();
assert!(lb.adjectives.contains(&Adjective::new("groussen", "grouss", "grousst")));
assert!(lb.nouns.contains(&Noun::new("kaz", Gender::Feminine)));
assert!(!lb.nouns.iter().any(|noun| noun.word.starts_with('#')));
assert!(!lb.adjectives.iter().any(|adjective| adjective.feminine.contains(':')));
}
#[test]
fn retain_filters_all_lists() {
let mut lb = sample();
lb.retain(|word| word.chars().count() <= 4);
assert!(lb.adjectives.is_empty());
assert_eq!(lb.adverbs.len(), 1);
assert_eq!(lb.nouns.len(), 3);
}
#[test]
fn retain_judges_adjectives_on_every_gender_form() {
let mut lb = sample();
lb.retain(|word| word.chars().count() <= 6);
assert!(lb.adjectives.is_empty());
let mut lb = sample();
lb.retain(|word| word.chars().count() <= 8);
assert_eq!(lb.adjectives.len(), 2);
}
#[cfg(feature = "default-words")]
#[test]
fn small_adjectives_are_regularly_formed() {
const IRREGULAR_NEUTER: &[&str] = &[];
for adjective in Petnames::small().adjectives.iter() {
assert!(
adjective.masculine.ends_with('n'),
"masculine {:?} should end in -n",
adjective.masculine
);
if IRREGULAR_NEUTER.contains(&adjective.feminine) {
continue;
}
let expected = if adjective.feminine.ends_with(['t', 'd']) {
alloc::string::String::from(adjective.feminine)
} else {
alloc::format!("{}t", adjective.feminine)
};
assert_eq!(
adjective.neuter, expected,
"neuter of {:?} should be {expected:?}",
adjective.feminine
);
}
}
}