rnglib/
lib.rs

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