hangman_solver_lib/solver/
pattern.rs1use std::char;
3use std::iter::zip;
4
5use crate::language::Language;
6use crate::solver::char_collection::CharCollection;
7use crate::solver::char_trait::ControlChars;
8use crate::solver::char_utils::CharUtils;
9use crate::solver::hangman_result::HangmanResult;
10#[cfg(feature = "wasm-bindgen")]
11use crate::solver::hangman_result::WasmHangmanResult;
12use crate::solver::infallible_char_collection::InfallibleCharCollection;
13
14use counter::Counter;
15use itertools::Itertools;
16
17#[cfg(feature = "wasm-bindgen")]
18use js_sys::JsString;
19
20#[allow(clippy::struct_field_names)]
21pub struct Pattern {
22 invalid_letters: Vec<char>,
23 pattern: Vec<char>,
24 first_letter: char,
25 letters_in_pattern_have_no_other_occurrences: bool,
27 known_letters_count: usize,
28 invalid_letters_all_ascii: bool,
29 invalid_ascii_letters: [bool; 128],
30}
31
32#[allow(dead_code)]
33impl Pattern {
34 #[must_use]
35 pub const fn invalid_letters(&self) -> &[char] {
36 self.invalid_letters.as_slice()
37 }
38
39 #[must_use]
40 pub const fn pattern(&self) -> &[char] {
41 self.pattern.as_slice()
42 }
43}
44
45#[expect(clippy::used_underscore_items)]
46impl Pattern {
47 #[inline]
48 pub fn new<E1, E2, Err: From<E1> + From<E2>>(
49 pattern: &(impl CharCollection<Error = E1> + ?Sized),
50 invalid_letters: &(impl CharCollection<Error = E2> + ?Sized),
51 letters_in_pattern_have_no_other_occurrences: bool,
52 ) -> Result<Self, Err> {
53 let mut known_letters_count = 0;
54 let mut pattern_as_chars: Vec<char> =
55 Vec::with_capacity(pattern.try_count_chars()?);
56
57 for ch in pattern.try_iter_chars()? {
58 let ch = ch?;
59 if ch.is_whitespace() {
60 continue;
61 }
62 if ch.is_wildcard() {
63 pattern_as_chars.push(char::WILDCARD);
64 continue;
65 }
66 known_letters_count += 1;
67 pattern_as_chars.extend(ch.to_lowercase());
68 }
69
70 let mut invalid_letters_vec: Vec<char> = invalid_letters
71 .try_iter_chars()?
72 .filter(|ch| {
73 !ch.as_ref()
74 .is_ok_and(|ch| ch.is_whitespace() || ch.is_wildcard())
75 })
76 .collect::<Result<_, _>>()?;
77
78 if letters_in_pattern_have_no_other_occurrences {
79 for ch in &pattern_as_chars {
80 if ch.is_normalised_wildcard() {
81 continue;
82 }
83 if invalid_letters_vec.contains(ch) {
84 continue;
85 }
86 invalid_letters_vec.push(*ch);
87 }
88 }
89
90 let first_letter = *pattern_as_chars.first().unwrap_or(&char::WILDCARD);
91
92 let mut invalid_ascii_letters = [false; 128];
93 let mut invalid_letters_all_ascii: bool = true;
94
95 for ch in &invalid_letters_vec {
96 if let Some(b) = ch
97 .to_ascii_char()
98 .map(usize::from)
99 .and_then(|ch| invalid_ascii_letters.get_mut(ch))
100 {
101 *b = true;
102 } else {
103 invalid_letters_all_ascii = false;
104 }
105 }
106
107 Ok(Self {
108 invalid_letters: invalid_letters_vec,
109 pattern: pattern_as_chars,
110 first_letter,
111 letters_in_pattern_have_no_other_occurrences,
112 known_letters_count,
113 invalid_ascii_letters,
114 invalid_letters_all_ascii,
115 })
116 }
117
118 #[inline]
119 #[must_use]
120 fn first_letter_is_wildcard(&self) -> bool {
121 debug_assert_eq!(
122 self.first_letter.is_wildcard(),
123 self.first_letter.is_normalised_wildcard()
124 );
125 self.first_letter.is_normalised_wildcard()
126 }
127
128 #[must_use]
129 #[inline]
130 fn first_letter_matches<CC: InfallibleCharCollection + ?Sized>(
131 &self,
132 word: &&CC,
133 ) -> bool {
134 debug_assert!(!self.first_letter_is_wildcard());
136 word.first_char() == Some(self.first_letter)
137 }
138
139 #[inline]
140 pub(super) fn letter_is_valid(&self, letter: char) -> bool {
141 !letter
142 .to_ascii_char()
143 .map(usize::from)
144 .and_then(|ch| self.invalid_ascii_letters.get(ch))
145 .copied()
146 .unwrap_or(false)
147 && (self.invalid_letters_all_ascii
148 || !self.invalid_letters.contains(&letter))
149 }
150
151 #[must_use]
152 #[inline]
153 fn matches<CC: InfallibleCharCollection + ?Sized>(
154 &self,
155 word: &&CC,
156 ) -> bool {
157 debug_assert_eq!(word.char_count(), self.pattern.len());
159 debug_assert_eq!(
161 char::RESERVED
162 .iter()
163 .filter(|ch| word.iter_chars().contains(ch))
164 .count(),
165 0
166 );
167 for (p, w) in zip(self.pattern.iter(), word.iter_chars()) {
168 if *p == char::WILDCARD {
169 if !self.letter_is_valid(w) {
170 return false;
171 }
172 } else if *p != w {
173 return false;
174 }
175 }
176 true
177 }
178
179 #[inline]
180 #[must_use]
181 fn known_letters_count(&self) -> usize {
182 debug_assert_eq!(
183 self.known_letters_count,
184 self.pattern
185 .iter()
186 .filter(|ch| !ch.is_normalised_wildcard())
187 .count()
188 );
189
190 self.known_letters_count
191 }
192
193 #[must_use]
194 #[inline]
195 fn _collect_count_and_create_letter_frequency<
196 'a,
197 'b,
198 CC: InfallibleCharCollection + ?Sized + 'a,
199 T: Iterator<Item = &'a CC>,
200 >(
201 &self,
202 words: &'b mut T,
203 max_words_to_collect: Option<usize>,
204 ) -> (u32, Counter<char, u32>, Vec<&'a CC>) {
205 let mut letter_counter: Counter<char, u32> = Counter::new();
206
207 let update_counter: fn(&mut Counter<char, u32>, &CC) =
208 if self.letters_in_pattern_have_no_other_occurrences {
209 |counter, word| counter.update(word.iter_chars().unique())
210 } else {
211 |counter, word| counter.update(word.iter_chars())
212 };
213
214 let mut words =
215 words.inspect(|word| update_counter(&mut letter_counter, word));
216
217 let (words_vec, additional_count): (Vec<&'a CC>, usize) =
218 if let Some(n) = max_words_to_collect {
219 (words.by_ref().take(n).collect(), words.count())
220 } else {
221 (words.collect(), 0)
222 };
223
224 let words_count = u32::try_from(additional_count + words_vec.len())
225 .unwrap_or(u32::MAX);
226
227 if self.letters_in_pattern_have_no_other_occurrences {
228 for letter in &self.pattern {
229 if let Some(count) = letter_counter.remove(letter) {
230 debug_assert_eq!(count, words_count);
231 }
232 }
233 } else {
234 for letter in self
235 .pattern
236 .iter()
237 .filter(|char| !char.is_normalised_wildcard())
238 {
239 if let Some(new_count) = letter_counter
240 .get(letter)
241 .and_then(|c| c.checked_sub(words_count))
242 .and_then(|c| if c == 0 { None } else { Some(c) })
243 {
244 letter_counter.insert(*letter, new_count);
245 } else {
246 letter_counter.remove(letter);
247 }
248 }
249 }
250
251 (words_count, letter_counter, words_vec)
252 }
253
254 #[must_use]
255 #[inline]
256 pub fn solve(
257 &self,
258 language: Language,
259 max_words_to_collect: Option<usize>,
260 ) -> HangmanResult {
261 let mut all_words = language.read_words(self.pattern.len()).into_iter();
262 let (possible_words, letter_frequency, matching_words_count) =
263 self._solve_internal(&mut all_words, max_words_to_collect);
264
265 let mut invalid: Vec<char> = self
266 .invalid_letters
267 .iter()
268 .filter(|ch| !self.pattern.contains(*ch))
269 .copied()
270 .collect();
271
272 invalid.sort_unstable();
273 HangmanResult {
274 input: self.pattern.iter().collect(),
275 invalid,
276 possible_words,
277 language,
278 letter_frequency,
279 matching_words_count,
280 }
281 }
282
283 #[must_use]
284 #[inline]
285 fn _solve_internal<
286 'a,
287 'b,
288 CC: InfallibleCharCollection + ?Sized + 'a,
289 T: Iterator<Item = &'a CC>,
290 >(
291 &self,
292 all_words: &'b mut T,
293 max_words_to_collect: Option<usize>,
294 ) -> (Vec<&'a CC>, Vec<(char, u32)>, u32) {
295 let (word_count, letter_frequency, words) =
296 if self.invalid_letters.is_empty()
297 && self.known_letters_count() == 0
298 {
299 self._collect_count_and_create_letter_frequency(
300 all_words,
301 max_words_to_collect,
302 )
303 } else if self.first_letter_is_wildcard() {
304 let mut filtered_words =
305 all_words.filter(|word| self.matches(word));
306 self._collect_count_and_create_letter_frequency(
307 &mut filtered_words,
308 max_words_to_collect,
309 )
310 } else {
311 let mut filtered_words = all_words
312 .skip_while(|word| !self.first_letter_matches(word))
313 .take_while(|word| self.first_letter_matches(word))
314 .filter(|word| self.matches(word));
315 self._collect_count_and_create_letter_frequency(
316 &mut filtered_words,
317 max_words_to_collect,
318 )
319 };
320
321 (words, letter_frequency.most_common_ordered(), word_count)
322 }
323}
324
325#[cfg(feature = "wasm-bindgen")]
326#[expect(clippy::used_underscore_items)]
327impl Pattern {
328 #[must_use]
329 #[allow(dead_code)]
330 pub fn solve_with_words<'a, 'b, T: Iterator<Item = &'a JsString>>(
331 &self,
332 all_words: &'b mut T,
333 max_words_to_collect: Option<usize>,
334 ) -> WasmHangmanResult {
335 let (possible_words, letter_frequency, matching_words_count) =
336 self._solve_internal(all_words, max_words_to_collect);
337
338 let mut invalid: Vec<char> = self
339 .invalid_letters
340 .iter()
341 .filter(|ch| !self.pattern.contains(*ch))
342 .copied()
343 .collect();
344
345 let mut letter_frequency_string: String = String::new();
346
347 for (char, count) in letter_frequency {
348 if !letter_frequency_string.is_empty() {
349 letter_frequency_string.push_str(", ");
350 }
351 letter_frequency_string.push(char);
352 letter_frequency_string.push_str(": ");
353 letter_frequency_string.push_str(&count.to_string());
354 }
355
356 invalid.sort_unstable();
357 WasmHangmanResult {
358 input: JsString::from(self.pattern.iter().collect::<String>()),
359 invalid: JsString::from(invalid.iter().collect::<String>()),
360 possible_words: possible_words
361 .into_iter()
362 .map(JsString::to_string)
363 .collect(),
364 letter_frequency: JsString::from(letter_frequency_string),
365 matching_words_count,
366 }
367 }
368}