Skip to main content

rnglib/
lib.rs

1#![warn(clippy::pedantic)]
2#![cfg_attr(test, allow(clippy::unwrap_used))]
3
4mod rng_joiner;
5mod rng_syllable;
6mod rng_syllables;
7mod rng_weighted_rnd;
8
9#[macro_use]
10extern crate bitflags;
11extern crate log;
12
13#[derive(Debug, Eq, PartialEq)]
14pub enum RNGError {
15    GenerationError,
16    InvalidLanguageFile,
17    ParsingError,
18    ReadError,
19}
20
21use anyhow::Result;
22use rand::{
23    distr::{Distribution, StandardUniform},
24    prelude::*,
25};
26use rust_embed::RustEmbed;
27use std::fmt;
28use std::str::FromStr;
29use titlecase::titlecase;
30
31use crate::rng_syllable::{Classification, Syllable};
32use crate::rng_syllables::Syllables;
33use crate::rng_weighted_rnd::{NORMAL_WEIGHT, SHORT_WEIGHT};
34
35/// RNG (Random Name Generator) is a library that generates random
36/// names based upon one of the available Languages.
37///
38/// # Usage:
39/// ```
40/// use rnglib::{RNG, Language};
41///
42/// let rng = RNG::try_from(&Language::Elven).unwrap();
43///
44/// let first_name = rng.generate_name();
45/// let last_name = rng.generate_name();
46///
47/// println!("{}: {} {}", rng.name, first_name, last_name)
48/// ```
49#[allow(clippy::upper_case_acronyms)]
50#[derive(Clone, Debug, PartialEq)]
51pub struct RNG {
52    pub name: String,
53    pub prefixes: Syllables,
54    pub centers: Syllables,
55    pub suffixes: Syllables,
56    pub bad_syllables: Vec<String>,
57}
58
59impl RNG {
60    /// Use if you want to return the RNG entity, even if there are issues with some
61    /// of the syllables. Otherwise, use `RNG::try_from`.
62    ///
63    /// # Errors
64    ///
65    /// Errors out if the language file is not able to be processed correctly.
66    pub fn new(language: &Language) -> Result<RNG, RNG> {
67        let Ok(rng) = RNG::process(language) else {
68            return Err(RNG::empty(language.to_string()));
69        };
70
71        if rng.is_valid() { Ok(rng) } else { Err(rng) }
72    }
73
74    /// # Errors
75    ///
76    /// Errors out if the language file is not able to be processed correctly.
77    pub fn new_from_file(filename: String) -> Result<RNG, RNGError> {
78        match std::fs::read_to_string(filename.clone()) {
79            Ok(f) => match std::str::from_utf8(f.as_ref()) {
80                Ok(s) => Ok(RNG::classify(s, filename)),
81                Err(_) => Err(RNGError::InvalidLanguageFile),
82            },
83            Err(_) => Err(RNGError::InvalidLanguageFile),
84        }
85    }
86
87    /// # Panics
88    ///
89    /// Panics if the embedded language file for the chosen language cannot be
90    /// loaded, which cannot happen with the `debug-embed` feature enabled.
91    #[must_use]
92    pub fn random() -> RNG {
93        let my_dialect_type: Language = rand::random();
94        RNG::process(&my_dialect_type).expect("embedded language files are always present")
95    }
96
97    fn process(language: &Language) -> Result<RNG, RNGError> {
98        let txt =
99            Asset::get(language.get_filename().as_str()).ok_or(RNGError::InvalidLanguageFile)?;
100        let lines =
101            std::str::from_utf8(txt.data.as_ref()).map_err(|_| RNGError::InvalidLanguageFile)?;
102        Ok(RNG::classify(lines, language.to_string()))
103    }
104
105    fn classify(lines: &str, name: String) -> RNG {
106        let mut rng = RNG::empty(name);
107
108        for line in lines.lines() {
109            if let Ok(sy) = Syllable::from_str(line) {
110                match sy.classification {
111                    Classification::Prefix => rng.prefixes.add(sy),
112                    Classification::Center => rng.centers.add(sy),
113                    Classification::Suffix => rng.suffixes.add(sy),
114                }
115            } else {
116                rng.bad_syllables.push(line.to_string());
117            }
118        }
119        rng
120    }
121
122    #[must_use]
123    pub fn empty(name: String) -> RNG {
124        RNG {
125            name,
126            prefixes: Syllables::new(),
127            centers: Syllables::new(),
128            suffixes: Syllables::new(),
129            bad_syllables: Vec::new(),
130        }
131    }
132
133    #[must_use]
134    pub fn is_empty(&self) -> bool {
135        self.name.is_empty()
136            && self.prefixes.is_empty()
137            && self.centers.is_empty()
138            && self.suffixes.is_empty()
139            && self.bad_syllables.is_empty()
140    }
141
142    #[must_use]
143    pub fn is_valid(&self) -> bool {
144        !self.name.is_empty()
145            && !self.prefixes.is_empty()
146            && !self.centers.is_empty()
147            && !self.suffixes.is_empty()
148            && self.bad_syllables.is_empty()
149    }
150
151    #[must_use]
152    pub fn generate_name(&self) -> String {
153        self.generate_name_by_count(NORMAL_WEIGHT.random())
154    }
155
156    /// Returns a vector of names based on the number passed in. Returns
157    /// short weighted names if `is_short` is set to true.
158    #[must_use]
159    pub fn generate_names(&self, number: usize, is_short: bool) -> Vec<String> {
160        let mut v: Vec<String> = Vec::new();
161
162        for _ in 0..number {
163            if is_short {
164                v.push(self.generate_short());
165            } else {
166                v.push(self.generate_name());
167            }
168        }
169
170        v
171    }
172
173    #[must_use]
174    pub fn generate_names_string(&self, n: usize, is_short: bool) -> String {
175        self.generate_names(n, is_short).join(" ")
176    }
177
178    #[must_use]
179    pub fn generate_short(&self) -> String {
180        self.generate_name_by_count(SHORT_WEIGHT.random())
181    }
182
183    #[must_use]
184    pub fn generate_name_by_count(&self, count: u8) -> String {
185        let name = self.generate_syllables_by_count(count).collapse();
186        titlecase(name.as_str())
187    }
188
189    #[must_use]
190    pub fn generate_syllables(&self) -> Syllables {
191        self.generate_syllables_by_count(NORMAL_WEIGHT.random())
192    }
193
194    /// # Panics
195    ///
196    /// Panics if the RNG's prefix, center, or suffix syllable sets are empty
197    /// or contain no syllable compatible with the one before it.
198    #[must_use]
199    pub fn generate_syllables_by_count(&self, mut syllable_count: u8) -> Syllables {
200        let mut syllables = Syllables::new();
201        let mut last = self
202            .prefixes
203            .get_random()
204            .expect("language has at least one prefix syllable")
205            .clone();
206        syllables.add(last.clone());
207
208        while syllable_count > 2 {
209            let center_syllables = self.centers.filter_from(last.jnext);
210            last = center_syllables
211                .get_random()
212                .expect("language has a center syllable compatible with the previous one")
213                .clone();
214            syllables.add(last.clone());
215            syllable_count -= 1;
216        }
217
218        let last_syllables = self.suffixes.filter_from(last.jnext);
219
220        syllables.add(
221            last_syllables
222                .get_random()
223                .expect("language has a suffix syllable compatible with the previous one")
224                .clone(),
225        );
226
227        syllables
228    }
229
230    #[must_use]
231    pub fn syllables(&self) -> Syllables {
232        let v = [
233            self.prefixes.all().clone(),
234            self.centers.all().clone(),
235            self.suffixes.all().clone(),
236        ]
237        .concat();
238        Syllables::new_from_vector(v)
239    }
240}
241
242impl TryFrom<&Language> for RNG {
243    type Error = RNGError;
244
245    fn try_from(language: &Language) -> Result<Self, Self::Error> {
246        RNG::process(language)
247    }
248}
249
250#[derive(RustEmbed)]
251#[folder = "src/languages/"]
252struct Asset;
253
254#[cfg(test)]
255#[allow(non_snake_case)]
256mod lib_tests {
257    use super::*;
258    use proptest::prelude::*;
259
260    #[test]
261    fn try_from() {
262        let rng = RNG::try_from(&Language::Fantasy).unwrap();
263
264        assert_eq!(rng.name, Language::Fantasy.to_string());
265        assert!(rng.bad_syllables.len() < 1);
266        assert!(rng.prefixes.len() > 0);
267        assert!(rng.centers.len() > 0);
268        assert!(rng.suffixes.len() > 0);
269    }
270
271    #[test]
272    fn try_from__demonic() {
273        let rng = RNG::new(&Language::Demonic).unwrap();
274
275        assert!(rng.bad_syllables.len() < 1);
276        assert!(rng.prefixes.len() > 0);
277        assert!(rng.centers.len() > 0);
278        assert!(rng.suffixes.len() > 0);
279    }
280
281    #[test]
282    fn try_from__goblin() {
283        let result = RNG::try_from(&Language::Goblin).unwrap();
284
285        assert_eq!(result.name, Language::Goblin.to_string());
286        assert!(result.bad_syllables.len() < 1);
287        assert!(result.prefixes.len() > 0);
288        assert!(result.centers.len() > 0);
289        assert!(result.suffixes.len() > 0);
290    }
291
292    #[test]
293    fn try_from__roman() {
294        let result = RNG::try_from(&Language::Roman).unwrap();
295
296        assert_eq!(result.name, Language::Roman.to_string());
297        assert!(result.bad_syllables.len() < 1);
298        assert!(result.prefixes.len() > 0);
299        assert!(result.centers.len() > 0);
300        assert!(result.suffixes.len() > 0);
301    }
302
303    #[test]
304    fn try_from__klingon() {
305        let result = RNG::try_from(&Language::Klingon).unwrap();
306
307        assert_eq!(result.name, Language::Klingon.to_string());
308        assert!(result.bad_syllables.len() < 1);
309        assert!(result.prefixes.len() > 0);
310        assert!(result.centers.len() > 0);
311        assert!(result.suffixes.len() > 0);
312    }
313
314    #[test]
315    fn try_from__german_curse() {
316        let result = RNG::try_from(&Language::GermanCurse).unwrap();
317
318        assert_eq!(result.name, Language::GermanCurse.to_string());
319        assert!(result.bad_syllables.len() < 1);
320        assert!(result.prefixes.len() > 0);
321        assert!(result.centers.len() > 0);
322        assert!(result.suffixes.len() > 0);
323    }
324
325    #[test]
326    fn try_from__fantasy_russian() {
327        let result = RNG::try_from(&Language::Фантазия).unwrap();
328
329        assert_eq!(result.name, Language::Фантазия.to_string());
330        assert!(result.bad_syllables.len() < 1);
331        assert!(result.prefixes.len() > 0);
332        assert!(result.centers.len() > 0);
333        assert!(result.suffixes.len() > 0);
334    }
335
336    #[test]
337    fn new_from_file() {
338        let filename = "src/languages/Test-micro.txt";
339
340        let rng = RNG::new_from_file(filename.to_string());
341        let result = rng.as_ref().unwrap();
342
343        assert!(!rng.is_err());
344        assert_eq!(result.name, filename.to_string());
345        assert_eq!(result.bad_syllables.len(), 0);
346        assert_eq!(result.prefixes.len(), 1);
347        assert_eq!(result.centers.len(), 1);
348        assert_eq!(result.suffixes.len(), 1);
349    }
350
351    #[test]
352    fn new_from_file__russian_goblin() {
353        let filename = "src/languages/Гоблин.txt";
354
355        let rng = RNG::new_from_file(filename.to_string());
356        let result = rng.as_ref().unwrap();
357
358        assert!(!rng.is_err());
359        assert_eq!(result.name, filename.to_string());
360        assert_eq!(result.bad_syllables.len(), 0);
361        assert_eq!(result.prefixes.len(), 19);
362        assert_eq!(result.centers.len(), 13);
363        assert_eq!(result.suffixes.len(), 16);
364    }
365
366    #[test]
367    fn new_from_file__with_error() {
368        let filename = "src/languages/none.txt";
369
370        let result = RNG::new_from_file(filename.to_string());
371
372        assert!(result.is_err());
373    }
374
375    #[test]
376    fn new_from_file__russian_fantasy() {
377        let filename = "src/languages/Фантазия.txt";
378
379        let rng = RNG::new_from_file(filename.to_string());
380        let result = rng.as_ref().unwrap();
381
382        assert!(!rng.is_err());
383        assert_eq!(result.name, filename.to_string());
384        assert_eq!(result.bad_syllables.len(), 0);
385        assert_eq!(result.prefixes.len(), 180);
386        assert_eq!(result.centers.len(), 157);
387        assert_eq!(result.suffixes.len(), 19);
388    }
389
390    #[test]
391    fn process_file__with_error() {
392        let filename = "src/languages/none.txt";
393
394        let result = RNG::new_from_file(filename.to_string());
395
396        assert!(result.is_err());
397    }
398
399    #[test]
400    fn classify() {
401        let raw = "-ваа +c\n-боо +c\n-гар\n-бар\n-дар\n-жар\n-вар\n-кра\n-гра\n-дра\n-зра\n-гоб\n-доб\n-роб\n-фоб\n-зоб\n-раг\n-наг\n-даг\nбра\nга\nда\nдо\nго\nзе\nша\nназ\nзуб\nзу\nна\nгор\nбу +c\n+быр\n+гыр\n+д";
402        let filename = "src/languages/goblinRU.txt".to_string();
403
404        let classified = RNG::classify(raw, filename.clone());
405
406        assert_eq!(classified.name, filename);
407        assert_eq!(classified.bad_syllables.len(), 0);
408        assert_eq!(classified.prefixes.len(), 19);
409        assert_eq!(classified.centers.len(), 13);
410        assert_eq!(classified.suffixes.len(), 3);
411    }
412
413    #[test]
414    fn classify__fantasy_russian() {
415        let raw = "-а +c\n-аб\n-ак\n-ац\n-ад\n-аф\n-ам\n-ан\n-ап\n-ар\n-ас\n-ат\n-ав\n-аз\n-аэль\n-аэл\n-ао\n-аэр\n-аш\n-арш +v";
416        let filename = "src/languages/goblinRU.txt".to_string();
417
418        let classified = RNG::classify(raw, filename.clone());
419
420        assert_eq!(classified.name, filename);
421        assert_eq!(classified.bad_syllables.len(), 0);
422        // assert_eq!(classified.prefixes.len(), 19);
423        // assert_eq!(classified.centers.len(), 13);
424        // assert_eq!(classified.suffixes.len(), 3);
425    }
426
427    fn create_min() -> RNG {
428        RNG {
429            name: "Min".to_string(),
430            prefixes: Syllables::new_from_array(&["a"]),
431            centers: Syllables::new_from_array(&["b"]),
432            suffixes: Syllables::new_from_array(&["c"]),
433            bad_syllables: vec![],
434        }
435    }
436
437    #[test]
438    fn generate_name() {
439        let min = create_min();
440
441        let chain: Vec<String> = (1..10).map(|_| min.generate_name()).collect();
442
443        chain
444            .iter()
445            .for_each(|name| assert!(name.as_str().starts_with("A")));
446        chain
447            .iter()
448            .for_each(|name| assert!(name.as_str().ends_with("c")));
449    }
450
451    #[test]
452    fn generate_names() {
453        let rng = RNG::try_from(&Language::Roman).unwrap();
454
455        let names = rng.generate_names(5, true);
456
457        assert_eq!(names.len(), 5);
458    }
459
460    #[test]
461    fn generate_names_string() {
462        let rng = RNG::try_from(&Language::Demonic).unwrap();
463
464        let names = rng.generate_names_string(12, true);
465
466        assert_eq!(names.split_whitespace().count(), 12);
467    }
468
469    #[test]
470    fn generate_short() {
471        let min = create_min();
472
473        let chain: Vec<String> = (1..10).map(|_| min.generate_short()).collect();
474
475        chain
476            .iter()
477            .for_each(|name| assert!(name.as_str().starts_with("A")));
478        chain
479            .iter()
480            .for_each(|name| assert!(name.as_str().ends_with("c")));
481    }
482
483    #[test]
484    fn generate_name_by_count() {
485        let min = create_min();
486
487        let chain: Vec<String> = (1..10)
488            .map(|_| min.generate_name_by_count(NORMAL_WEIGHT.random()))
489            .collect();
490
491        chain
492            .iter()
493            .for_each(|name| assert!(name.as_str().starts_with("A")));
494        chain
495            .iter()
496            .for_each(|name| assert!(name.as_str().ends_with("c")));
497    }
498
499    #[test]
500    fn generate_syllables() {
501        let rng = RNG::try_from(&Language::Elven).unwrap();
502        let non: Vec<u8> = vec![0, 1, 6, 7, 8];
503
504        let chain: Vec<Syllables> = (1..10).map(|_| rng.generate_syllables()).collect();
505
506        chain
507            .iter()
508            .for_each(|i| assert!(NORMAL_WEIGHT.counts.contains(&(i.len() as u8))));
509        chain
510            .iter()
511            .for_each(|i| assert!(!non.contains(&(i.len() as u8))));
512    }
513
514    #[test]
515    fn is_empty() {
516        assert!(RNG::empty("".to_string()).is_empty());
517        assert!(!create_min().is_empty());
518    }
519
520    #[test]
521    fn is_valid() {
522        let min = create_min();
523        assert!(min.is_valid())
524    }
525
526    #[test]
527    fn is_valid__not() {
528        let bad = RNG {
529            name: "bad".to_string(),
530            prefixes: Syllables::new(),
531            centers: Syllables::new(),
532            suffixes: Syllables::new(),
533            bad_syllables: vec!["#$@!".to_string()],
534        };
535        assert!(!bad.is_valid())
536    }
537
538    #[test]
539    fn generate_syllables_by_count__elven_two() {
540        general_generate_dialects_asserts(Language::Elven, 2);
541    }
542
543    #[test]
544    fn generate_syllables_by_count__elven_three() {
545        general_generate_dialects_asserts(Language::Elven, 3);
546    }
547
548    #[test]
549    fn generate_syllables_by_count__elven_four() {
550        general_generate_dialects_asserts(Language::Elven, 4);
551    }
552
553    #[test]
554    fn generate_syllables_by_count__elven_five() {
555        general_generate_dialects_asserts(Language::Elven, 5);
556    }
557
558    #[test]
559    fn generate_syllables_by_count__fantasy_two() {
560        general_generate_dialects_asserts(Language::Fantasy, 2);
561    }
562
563    #[test]
564    fn generate_syllables_by_count__fantasy_three() {
565        general_generate_dialects_asserts(Language::Fantasy, 3);
566    }
567
568    #[test]
569    fn generate_syllables_by_count__fantasy_four() {
570        general_generate_dialects_asserts(Language::Fantasy, 4);
571    }
572
573    #[test]
574    fn generate_syllables_by_count__fantasy_five() {
575        general_generate_dialects_asserts(Language::Fantasy, 5);
576    }
577
578    #[test]
579    fn generate_syllables_by_count__goblin_two() {
580        general_generate_dialects_asserts(Language::Goblin, 2);
581    }
582
583    #[test]
584    fn generate_syllables_by_count__goblin_three() {
585        general_generate_dialects_asserts(Language::Goblin, 3);
586    }
587
588    #[test]
589    fn generate_syllables_by_count__goblin_four() {
590        general_generate_dialects_asserts(Language::Goblin, 4);
591    }
592
593    #[test]
594    fn generate_syllables_by_count__goblin_five() {
595        general_generate_dialects_asserts(Language::Goblin, 5);
596    }
597
598    #[test]
599    fn generate_syllables_by_count__roman_two() {
600        general_generate_dialects_asserts(Language::Roman, 2);
601    }
602
603    #[test]
604    fn generate_syllables_by_count__roman_three() {
605        general_generate_dialects_asserts(Language::Roman, 3);
606    }
607
608    #[test]
609    fn generate_syllables_by_count__roman_four() {
610        general_generate_dialects_asserts(Language::Roman, 4);
611    }
612
613    #[test]
614    fn generate_syllables_by_count__roman_five() {
615        general_generate_dialects_asserts(Language::Roman, 5);
616    }
617
618    // region assert functions
619    fn general_generate_dialects_asserts(language: Language, count: usize) {
620        for _ in 0..9 {
621            let rng = RNG::try_from(&language).unwrap();
622
623            let name = rng.generate_syllables_by_count(count as u8);
624
625            general_generate_syllables_asserts(&rng, &name);
626            assert_eq!(name.len(), count);
627        }
628    }
629
630    fn general_generate_syllables_asserts(rng: &RNG, syllables: &Syllables) {
631        assert!(rng.prefixes.contains(syllables.first().unwrap()));
632        assert!(!rng.centers.contains(syllables.first().unwrap()));
633        assert!(!rng.suffixes.contains(syllables.first().unwrap()));
634
635        let count = syllables.len();
636        let mut guard = 1;
637        while guard < count - 1 {
638            guard += 1;
639            assert!(!rng.prefixes.contains(syllables.get(guard - 1).unwrap()));
640            assert!(rng.centers.contains(syllables.get(guard - 1).unwrap()));
641            assert!(!rng.suffixes.contains(syllables.get(guard - 1).unwrap()));
642        }
643
644        assert!(!rng.prefixes.contains(syllables.last().unwrap()));
645        assert!(!rng.centers.contains(syllables.last().unwrap()));
646        assert!(rng.suffixes.contains(syllables.last().unwrap()));
647    }
648    // endregion
649
650    proptest! {
651        #[test]
652        fn test_gen_rnd_syllable_count(_ in 0..100i32) {
653            let count = NORMAL_WEIGHT.random();
654            assert!((count < 6) && (count > 1), "count of {} should be less than 6 and greater than 1", count);
655        }
656    }
657}
658
659// region Language
660
661#[derive(Clone, Debug, PartialEq)]
662pub enum Language {
663    Curse,
664    Demonic,
665    Elven,
666    Эльфийский,
667    Fantasy,
668    Фантазия,
669    GermanCurse,
670    Goblin,
671    Гоблин,
672    Klingon,
673    Roman,
674    Римский,
675}
676
677impl fmt::Display for Language {
678    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
679        match self {
680            // The language file is named after the upstream Ruby project's
681            // german-curse.txt; a hyphen can't appear in a variant name.
682            Language::GermanCurse => write!(f, "German-curse"),
683            _ => write!(f, "{self:?}"),
684        }
685    }
686}
687
688impl Distribution<Language> for StandardUniform {
689    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Language {
690        match rng.random_range(0..5) {
691            0 => Language::Demonic,
692            1 => Language::Elven,
693            2 => Language::Fantasy,
694            3 => Language::Goblin,
695            _ => Language::Roman,
696        }
697    }
698}
699
700impl Language {
701    #[must_use]
702    pub fn get_filename(&self) -> String {
703        format!("{self}.txt")
704    }
705
706    #[must_use]
707    pub fn get_path(&self) -> String {
708        format!("./src/languages/{}", self.get_filename())
709    }
710}
711
712/// This may come in handy some day.
713#[derive(Debug, Clone, PartialEq)]
714pub struct BadLanguage;
715
716impl fmt::Display for BadLanguage {
717    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
718        write!(f, "Invalid Language")
719    }
720}
721
722#[cfg(test)]
723#[allow(non_snake_case)]
724mod test_language {
725    use super::*;
726
727    #[test]
728    fn to_filename() {
729        assert_eq!(String::from("Elven.txt"), Language::Elven.get_filename());
730    }
731
732    #[test]
733    fn to_filename__german_curse() {
734        assert_eq!(
735            String::from("German-curse.txt"),
736            Language::GermanCurse.get_filename()
737        );
738    }
739
740    #[test]
741    fn to_string() {
742        assert_eq!(String::from("Elven"), Language::Elven.to_string());
743    }
744
745    #[test]
746    fn get_path() {
747        assert_eq!(
748            "./src/languages/Fantasy.txt".to_string(),
749            Language::Fantasy.get_path()
750        );
751    }
752}
753// endregion