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 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 } 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 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}