witty_phrase_generator/
lib.rs

1use rand::prelude::{ ThreadRng, thread_rng, IteratorRandom };
2use rand::Rng;
3use rand::seq::SliceRandom;
4
5use std::cell::RefCell;
6
7/// Base witty phrase generator struct
8///
9/// Make a new generator using the default wordlists with new().
10pub struct WPGen {
11    rng: RefCell<ThreadRng>,
12    words_intensifiers: Vec<&'static str>,
13    words_adjectives: Vec<&'static str>,
14    words_nouns: Vec<&'static str>,
15}
16
17impl WPGen {
18    pub fn new() -> WPGen {
19        let words_intensifiers = include_str!("intensifiers.txt");
20        let words_adjectives   = include_str!("adjectives.txt")  ;
21        let words_nouns        = include_str!("nouns.txt")       ;
22
23        let words_intensifiers = words_intensifiers.lines().collect::<Vec<&'static str>>();
24        let words_adjectives   = words_adjectives  .lines().collect::<Vec<&'static str>>();
25        let words_nouns        = words_nouns       .lines().collect::<Vec<&'static str>>();
26
27        WPGen { 
28            rng: RefCell::new(thread_rng()),
29            words_intensifiers,
30            words_adjectives  ,
31            words_nouns       ,
32        }
33    }
34
35    fn create_format(words: usize) -> Vec<usize> {
36        // TODO: return Vec<ENUM{ intensifier, adjective, noun }> instead of usize
37        // TODO: convert with_words fn to use this also
38
39        let mut ret = vec![0; words+1];
40        let mut n = 1;
41
42        if words > 3 { ret[4] = 3 }
43        if words > 2 { ret[n] = 1; n += 1; }
44        if words > 1 { ret[n] = 2; n += 1; }
45        if words > 0 { ret[n] = 3; }
46         
47        ret
48    }
49
50    fn generate_backtracking(&self,
51                             len_min: usize,
52                             len_max: usize,
53                             dep: usize,
54                             dict: &[Vec<&&'static str>; 4],
55                             format: &Vec<usize>,
56                            ) -> Option<Vec<&'static str>> {
57        let pool = &dict[format[dep]];
58        
59        //let upper_bound = {
60        //    let [mut l, mut r] = [0, pool.len()];
61        //    while r - l > 1 {
62        //        let m = l+(r-l>>1);
63        //        if pool[m].len() <= len_max +1 { l = m }
64        //        else { r = m }
65        //    };
66        //    l
67        //};
68        // TODO: is binary search even faster on such short wordlists?
69        let mut upper_bound = 0;
70        while upper_bound < pool.len() && pool[upper_bound].len() <= len_max { upper_bound += 1; }
71
72
73        let pool = pool[0..upper_bound]
74                .choose_multiple(&mut *self.rng.borrow_mut(), upper_bound)
75                .collect::<Vec<&&&str>>();
76
77        for selected in pool {
78            assert!(selected.len() <= len_max);
79
80            if dep >= format.len()-1 { // last iteration (base case)
81                if selected.len() < len_min { continue } // would wrap all the way around 
82                return Some(vec![selected])
83            }
84
85            if let Some(mut suf) = self.generate_backtracking(
86                    (len_min as i32 - selected.len() as i32).max(0) as usize,
87                    len_max - selected.len(),
88                    dep+1, dict, format) {
89                suf.push(selected);
90                return Some(suf);
91            }
92        };
93        None
94    }
95
96    /// Generate the requested phrases if possible
97    ///
98    /// Returns None if the conditions could not be satisfied
99    ///
100    /// len_min and len_max denote the minimum and maximum number of
101    /// letters in all the concattenated words respectively. Note 
102    /// these lengths do not include seperator characters if they 
103    /// will be inserted.
104    ///
105    /// All words (even across phrases) will start with start_char
106    /// if it is provided. To allow different phrases to alliterate
107    /// with different letters, use with_phrasewise_alliteration
108    /// instead. 
109    pub fn generic(&self,
110                   words: usize,
111                   count: usize,
112                   len_min: Option<usize>,
113                   len_max: Option<usize>,
114                   word_len_max: Option<usize>,
115                   start_char: Option<char>
116                ) -> Option<Vec<Vec<&'static str>>> {
117
118        let len_min = len_min.unwrap_or(0);
119        let len_max = len_max.unwrap_or(usize::MAX);
120        let word_len_max = word_len_max.unwrap_or(len_max / (words-1));
121
122        if len_max < len_min        { return None }
123        if words > 4 || words == 0  { return None }
124
125        // convert to references
126        let words_intensifiers = self.words_intensifiers.iter().map(|x| x).collect::<Vec<&&'static str>>();
127        let words_adjectives   = self.words_adjectives  .iter().map(|x| x).collect::<Vec<&&'static str>>();
128        let words_nouns        = self.words_nouns       .iter().map(|x| x).collect::<Vec<&&'static str>>();
129
130        // dictionary that we can recurse over
131        let mut dict = [Vec::new(), words_intensifiers, words_adjectives, words_nouns];
132
133        for list in &mut dict {
134            // filter words we know we can't use
135            if let Some(c) = start_char {
136                list.retain(|s| s.chars().nth(0).expect("empty word found!") == c);
137            }
138            list.retain(|s| s.len() <= word_len_max);  // filter out words that are already longer than len_max; TODO: too restrictive sometimes
139            list.shuffle(&mut *self.rng.borrow_mut());      // shuffle all the available words 
140            list.sort_by(|a, b: &&&str| a.len().cmp(&b.len()));     // sort by length (stable sort, so still shuffled) for easier length matching
141        }
142
143        let mut ret = vec![vec![""; words]; count];
144
145        for i in 0..count {
146            if let Some(mut vec) = self.generate_backtracking(len_min, len_max, 1, &dict, &WPGen::create_format(words)) {
147                vec.reverse();
148                ret[i] = vec;
149            } else {
150                return None;
151            }
152        }
153        Some(ret) 
154    }
155
156    /// Generate the requested phrases if possible
157    ///
158    /// Returns None if the conditions could not be satisfied
159    ///
160    /// len_min and len_max denote the minimum and maximum number of
161    /// letters in all the concattenated words respectively. Note 
162    /// these lengths do not include seperator characters if they 
163    /// will be inserted.
164    ///
165    /// Each phrase will alliterate internally, but different
166    /// phrases may start with different letters. To specify
167    /// what letter to start with, use generic() instead.
168    pub fn with_phrasewise_alliteration(&self,
169                   words: usize,
170                   count: usize,
171                   len_min: Option<usize>,
172                   len_max: Option<usize>,
173                   word_len_max: Option<usize>,
174                ) -> Option<Vec<Vec<&'static str>>> {
175
176        let mut ret = Vec::new();
177        ret.reserve_exact(count);
178        for _ in 0..count {
179            ret.append(&mut loop {
180                let char = (*self.rng.borrow_mut()).gen_range(b'a'..b'z'+1) as char;
181                if let Some(p) = self.generic(words, 1, len_min, len_max, word_len_max, Some(char)) {
182                    break p
183                }
184            });
185        }
186        Some(ret)
187    }
188
189    /// Generate a witty phrase with either 1, 2, or 3 words
190    ///
191    /// returns None when no phrase could be generated (eg. if one of the wordlists is empty)
192    pub fn with_words(&self, words: usize) -> Option<Vec<&'static str>> {
193        let mut ret = vec![""; words];
194        let mut n = 0;
195
196        if words > 3 { ret[3] = self.words_nouns       .iter().choose(&mut *self.rng.borrow_mut())?; }
197
198        if words > 2 { ret[n] = self.words_intensifiers.iter().choose(&mut *self.rng.borrow_mut())?; n += 1; }
199        if words > 1 { ret[n] = self.words_adjectives  .iter().choose(&mut *self.rng.borrow_mut())?; n += 1; }
200        if words > 0 { ret[n] = self.words_nouns       .iter().choose(&mut *self.rng.borrow_mut())?; }
201
202        Some(ret)
203    }
204}
205