lib_wpsr/
anagram.rs

1use std::collections::{HashMap, VecDeque};
2
3use colorful::Colorful;
4
5use crate::{DEFAULT_SOURCE_DIR, DEFAULT_WORDS_SOURCE_FILE, Error, WordFilters};
6
7const DEFAULT_LIMIT: usize = 200;
8
9#[derive(Debug, Default)]
10pub struct Anagram {
11    settings: HashMap<String, String>,
12    letters: Vec<char>,
13    word_source: String,
14    words: Vec<String>,
15    solutions: Vec<String>,
16    distribution: HashMap<usize, i32>,
17    limit: Option<usize>,
18}
19
20impl Anagram {
21    pub fn new(letters: &str, settings: HashMap<String, String>) -> Result<Self, Error> {
22        if letters.len() < 3 || letters.len() > 26 {
23            return Err(Error::TooFewOrManyLetters(letters.len()));
24        }
25
26        let letters = letters
27            .chars()
28            .map(|l| l.to_ascii_lowercase())
29            .collect::<Vec<char>>();
30
31        Ok(Self {
32            settings,
33            letters,
34            ..Default::default()
35        })
36    }
37
38    pub fn set_word_source(&mut self, dir: Option<String>, file: Option<String>) -> &mut Self {
39        // Setup settings
40        let mut src_directory = self
41            .settings
42            .get("source_dir")
43            .map_or(DEFAULT_SOURCE_DIR, |v| v)
44            .to_string();
45        let mut src_file = self
46            .settings
47            .get("source_words_file")
48            .map_or(DEFAULT_WORDS_SOURCE_FILE, |v| v)
49            .to_string();
50
51        if let Some(sd) = dir {
52            src_directory = sd;
53        };
54        if let Some(sf) = file {
55            src_file = sf;
56        };
57
58        let src = format!("{}/{}", src_directory.clone(), src_file.clone());
59        println!("Using word list: {src}");
60        tracing::info!("Using word list: {}", src);
61
62        self.word_source = src;
63
64        self
65    }
66
67    pub fn load_words(&mut self) -> &mut Self {
68        let mut words = Vec::new();
69
70        for line in std::fs::read_to_string(&self.word_source)
71            .expect("Failed to read words file")
72            .lines()
73        {
74            if !line.is_empty() {
75                let ws = line.split_whitespace();
76                for w in ws {
77                    words.push(w.to_string());
78                }
79            }
80        }
81
82        self.words = words;
83
84        self
85    }
86
87    #[tracing::instrument(skip(self))]
88    pub fn find_solutions(&mut self) -> Result<&mut Self, Error> {
89        tracing::trace!("{}", self.letters.clone().iter().collect::<String>());
90
91        let mut filtered = self.words.clone();
92
93        if self.letters.contains(&' ') {
94            tracing::trace!("Found a space in letters");
95            let anagram = self.letters.iter().collect::<String>();
96            filtered.filter_includes_only_letters(
97                &self
98                    .letters
99                    .iter()
100                    .filter(|c| c != &&' ')
101                    .collect::<String>(),
102            );
103            let mut finder = AnagramFinder::new(filtered.clone(), &anagram);
104            if let Some(limit) = self.limit {
105                finder.set_limit(limit);
106            }
107            let anagrams = finder.find_anagrams();
108            filtered = anagrams;
109            // let mut solutions = Vec::new();
110        } else {
111            tracing::debug!("{} words found", filtered.len());
112            filtered.filter_includes_same_letters(&self.letters.iter().collect::<String>());
113            tracing::debug!("{} words found", filtered.len());
114        }
115
116        let final_list = filtered
117            .iter()
118            .cloned()
119            .inspect(|w| {
120                self.count_solution(w.len());
121            })
122            .collect::<Vec<String>>();
123
124        tracing::trace!("Final list: {:#?}", final_list);
125        self.solutions = final_list;
126
127        Ok(self)
128    }
129
130    pub fn count_solution(&mut self, chain_length: usize) -> &mut Self {
131        if let Some(count) = self.distribution.get(&chain_length) {
132            let v = count + 1;
133            self.distribution.insert(chain_length, v);
134        } else {
135            self.distribution.insert(chain_length, 1);
136        }
137
138        self
139    }
140
141    pub fn word_source_string(&self) -> String {
142        let s1 = "Using words sourced from ".light_cyan().dim().to_string();
143        let s2 = self.word_source.clone().light_cyan().bold().to_string();
144        format!("{s1}{s2}")
145    }
146
147    pub fn distribution_string(&self) -> String {
148        let mut s = String::new();
149        let mut distributions = self.distribution.iter().collect::<Vec<_>>();
150        distributions.sort_by(|a, b| a.0.cmp(b.0));
151
152        for d in distributions {
153            s.push_str(&format!(
154                "  - {:3.0} solutions with {:2.0} characters\n",
155                d.1, d.0
156            ));
157        }
158        s
159    }
160
161    pub fn solutions_title(&self) -> String {
162        let intro = "Words using the letters ";
163        let mut ul = String::new();
164        for _ in 0..(intro.len() + self.letters.len()) {
165            ul.push('‾');
166        }
167
168        let summary = format!(
169            "{}{}",
170            intro.yellow().bold(),
171            self.letters.iter().collect::<String>().blue().bold()
172        );
173        format!("{}\n{}", summary, ul.bold().yellow())
174    }
175
176    pub fn solutions_string(&self) -> String {
177        let mut s = String::new();
178        let mut solutions = self
179            .solutions
180            .iter()
181            .map(|s| (s.len(), s))
182            .collect::<Vec<_>>();
183        solutions.sort_by(|a, b| a.0.cmp(&b.0));
184
185        let mut word_length = solutions.first().unwrap_or(&(0, &"".to_string())).0;
186
187        s.push_str("  ");
188        s.push_str(
189            &format!(
190                "{} Solutions with {} letters.",
191                self.distribution.get(&word_length).unwrap_or(&0),
192                word_length
193            )
194            .underlined()
195            .yellow()
196            .to_string(),
197        );
198        s.push_str("\n\n");
199
200        for solution in solutions {
201            if solution.0 != word_length {
202                word_length = solution.0;
203                s.push_str("\n  ");
204                s.push_str(
205                    &format!(
206                        "{} Solutions with {} letters.",
207                        self.distribution.get(&word_length).unwrap_or(&0),
208                        word_length
209                    )
210                    .underlined()
211                    .yellow()
212                    .to_string(),
213                );
214                s.push_str("\n\n");
215            }
216            s.push_str(&format!("    {}\n", solution.1));
217        }
218        s
219    }
220}
221
222#[derive(Debug, Default)]
223struct AnagramFinder {
224    words: Vec<String>,
225    anagram: String,
226    new_anagram: String,
227    anagrams: Vec<String>,
228    limit: Option<usize>,
229}
230
231impl AnagramFinder {
232    fn new(words: Vec<String>, anagram: &str) -> Self {
233        Self {
234            words,
235            anagram: anagram.to_string(),
236            ..Default::default()
237        }
238    }
239
240    fn set_new_anagram(&mut self, value: &str) {
241        self.new_anagram = value.to_string();
242    }
243
244    fn set_limit(&mut self, value: usize) {
245        self.limit = Some(value);
246    }
247
248    #[tracing::instrument(skip(self))]
249    fn find_anagrams(&mut self) -> Vec<String> {
250        tracing::trace!("Anagram phrases using letters `{}`", self.anagram);
251
252        // Get the base distribution of letters in the anagram as each letter can only be used once
253        let mut base_letter_pool = HashMap::new();
254        for letter in self.anagram.chars() {
255            if letter != ' ' {
256                *base_letter_pool.entry(letter).or_insert(0) += 1;
257            }
258        }
259
260        let base_list = self
261            .words
262            .clone()
263            .filter_includes_any_letters(&self.anagram);
264
265        tracing::trace!("base_list: {:?} with anagram {}", base_list, &self.anagram);
266
267        let mut base_list = base_list.filter_includes_specific_letters_in_volume(&self.anagram);
268        tracing::trace!(
269            "specific base_list: {:?} with anagram `{}`",
270            base_list,
271            &self.anagram
272        );
273
274        base_list.sort_by_key(|a| a.len());
275        base_list.reverse();
276        tracing::debug!("filtered, sorted and reversed: {:?}", base_list);
277
278        let mut base_queue = VecDeque::from(base_list);
279
280        loop {
281            let limit = if let Some(limit) = self.limit {
282                limit
283            } else {
284                DEFAULT_LIMIT
285            };
286            if self.anagrams.len() >= limit {
287                break;
288            }
289            let Some(word) = base_queue.pop_front() else {
290                break;
291            };
292
293            let mut build_anagram = self.new_anagram.clone();
294            if !build_anagram.is_empty() {
295                build_anagram.push(' ');
296            }
297            build_anagram.push_str(&word);
298
299            tracing::debug!("phrase: {:?}", build_anagram);
300
301            let mut letter_pool = base_letter_pool.clone();
302            for letter in word.chars() {
303                *letter_pool.entry(letter).or_insert(0) -= 1;
304            }
305            letter_pool.retain(|_, v| *v > 0);
306
307            if letter_pool.is_empty() {
308                self.anagrams.push(build_anagram);
309                continue;
310            }
311
312            tracing::trace!("New letter pool: {:?}", letter_pool);
313            let new_anagram = letter_pool.into_keys().collect::<String>();
314            let mut new_finder = AnagramFinder::new(self.words.clone(), &new_anagram);
315            new_finder.set_new_anagram(&build_anagram);
316            let anagrams = new_finder.find_anagrams();
317            self.anagrams.extend(anagrams);
318        }
319
320        self.anagrams.clone()
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_anagram_phrases_using_letters() {
330        let words = [
331            "apt",
332            "flowerpot",
333            "followers",
334            "in",
335            "pom",
336            "min",
337            "slain",
338            "slam",
339            "tap",
340            "tin",
341            "tip",
342            "waterfalls",
343        ];
344
345        let anagram = "parliament of owls";
346
347        let words = words.iter().map(|w| w.to_string()).collect::<Vec<String>>();
348
349        let mut anagrams = AnagramFinder::new(words, anagram);
350        let _ = anagrams.find_anagrams();
351
352        let anagrams = anagrams.anagrams;
353
354        assert_eq!(
355            anagrams,
356            vec![
357                String::from("waterfalls pom in"),
358                String::from("waterfalls in pom"),
359                String::from("followers tap min"),
360                String::from("followers min tap"),
361                String::from("followers min apt"),
362                String::from("followers apt min"),
363                String::from("flowerpot slam in"),
364                String::from("flowerpot in slam")
365            ]
366        );
367    }
368}