1use std::collections::HashMap;
2
3use colorful::Colorful;
4
5use crate::{DEFAULT_SOURCE_DIR, DEFAULT_WORDS_SOURCE_FILE, Error, WordFilters};
6
7#[derive(Debug, Default)]
8pub struct Words {
9 settings: HashMap<String, String>,
10 letters: Vec<char>,
11 word_source: String,
12 words: Vec<String>,
13 solutions: Vec<String>,
14 max: usize,
15 required: Option<String>,
16 pangram: bool,
17 distribution: HashMap<usize, i32>,
18}
19
20impl Words {
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 pub fn set_max_solutions(&mut self, value: usize) -> &mut Self {
88 self.max = value;
89 self
90 }
91
92 pub fn set_required(&mut self, value: Option<String>) -> &mut Self {
93 self.required = value;
94 self
95 }
96
97 pub fn set_pangram(&mut self, value: bool) -> &mut Self {
98 self.pangram = value;
99 self
100 }
101
102 #[tracing::instrument(skip(self))]
103 pub fn find_solutions(&mut self) -> Result<&mut Self, Error> {
104 tracing::info!("Get un-shuffled word list");
105 let all_letters = "abcdefghijklmnopqrstuvwxyz";
106 let excluded_letters = all_letters
107 .chars()
108 .filter(|&c| !self.letters.contains(&c))
109 .collect::<String>();
110
111 let words = self.words.clone();
112 println!("{} words found", words.len());
113 let mut filtered = words.filter_excludes_letters(&excluded_letters);
114 if let Some(required) = &self.required {
115 filtered = filtered.filter_includes_any_letters(required);
116 }
117 if self.pangram {
118 filtered =
119 filtered.filter_includes_all_letters(&self.letters.iter().collect::<String>());
120 }
121 println!("{} words found", filtered.len());
122
123 filtered.sort_by(|a, b| {
124 let a_len = a.len();
125 let b_len = b.len();
126 b_len.cmp(&a_len)
127 });
128
129 let final_list = filtered
130 .iter()
131 .take(self.max)
132 .cloned()
133 .inspect(|w| {
134 self.count_solution(w.len());
135 })
136 .collect::<Vec<String>>();
137
138 self.solutions = final_list;
139
140 Ok(self)
141 }
142
143 pub fn count_solution(&mut self, chain_length: usize) -> &mut Self {
144 if let Some(count) = self.distribution.get(&chain_length) {
145 let v = count + 1;
146 self.distribution.insert(chain_length, v);
147 } else {
148 self.distribution.insert(chain_length, 1);
149 }
150
151 self
152 }
153
154 pub fn word_source_string(&self) -> String {
155 let s1 = "Using words sourced from ".light_cyan().dim().to_string();
156 let s2 = self.word_source.clone().light_cyan().bold().to_string();
157 format!("{s1}{s2}")
158 }
159
160 pub fn distribution_string(&self) -> String {
161 let mut s = String::new();
162 let mut distributions = self.distribution.iter().collect::<Vec<_>>();
163 distributions.sort_by(|a, b| a.0.cmp(b.0));
164
165 for d in distributions {
166 s.push_str(&format!(
167 " - {:3.0} solutions with {:2.0} words\n",
168 d.1, d.0
169 ));
170 }
171 s
172 }
173
174 pub fn solutions_title(&self) -> String {
175 let intro = "Words using the letters ";
176 let mut ul = String::new();
177 for _ in 0..(intro.len() + self.letters.len()) {
178 ul.push('‾');
179 }
180
181 let summary = format!(
182 "{}{}",
183 intro.yellow().bold(),
184 self.letters.iter().collect::<String>().blue().bold()
185 );
186 format!("{}\n{}", summary, ul.bold().yellow())
187 }
188
189 pub fn solutions_string(&self) -> String {
190 let mut s = String::new();
191 let mut solutions = self
192 .solutions
193 .iter()
194 .map(|s| (s.len(), s))
195 .collect::<Vec<_>>();
196 solutions.sort_by(|a, b| a.0.cmp(&b.0));
197
198 let mut word_length = solutions.first().unwrap_or(&(0, &"".to_string())).0;
199
200 s.push_str(" ");
201 s.push_str(
202 &format!(
203 "{} Solutions with {} words.",
204 self.distribution.get(&word_length).unwrap_or(&0),
205 word_length
206 )
207 .underlined()
208 .yellow()
209 .to_string(),
210 );
211 s.push_str("\n\n");
212
213 for solution in solutions {
214 if solution.0 != word_length {
215 word_length = solution.0;
216 s.push_str("\n ");
217 s.push_str(
218 &format!(
219 "{} Solutions with {} words.",
220 self.distribution.get(&word_length).unwrap_or(&0),
221 word_length
222 )
223 .underlined()
224 .yellow()
225 .to_string(),
226 );
227 s.push_str("\n\n");
228 }
229 s.push_str(&format!(" {}\n", solution.1));
230 }
231 s
232 }
233}