rs_wordle_solver/data.rs
1use crate::results::*;
2use std::collections::HashMap;
3use std::fmt::{Debug, Display};
4use std::hash::Hash;
5use std::io;
6use std::ops::Deref;
7use std::result::Result;
8use std::sync::Arc;
9
10/// A letter along with its location in the word.
11///
12/// ```
13/// use rs_wordle_solver::details::LocatedLetter;
14///
15/// let word = "abc";
16///
17/// let mut located_letters = Vec::new();
18/// for (index, letter) in word.char_indices() {
19/// located_letters.push(LocatedLetter::new(letter, index as u8));
20/// }
21///
22/// assert_eq!(&located_letters, &[
23/// LocatedLetter::new('a', 0),
24/// LocatedLetter::new('b', 1),
25/// LocatedLetter::new('c', 2),
26/// ]);
27/// ```
28#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct LocatedLetter {
31 pub letter: char,
32 /// The zero-based location (i.e. index) for this letter in a word.
33 pub location: u8,
34}
35
36impl LocatedLetter {
37 pub fn new(letter: char, location: u8) -> LocatedLetter {
38 LocatedLetter { letter, location }
39 }
40}
41
42/// Contains all the possible words for a Wordle game.
43#[derive(Clone, Debug, PartialEq)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45pub struct WordBank {
46 pub(crate) all_words: Vec<Arc<str>>,
47 word_length: usize,
48}
49
50impl WordBank {
51 /// Constructs a new `WordBank` struct by reading words from the given reader.
52 ///
53 /// The reader should provide one word per line. Each word will be trimmed and converted to
54 /// lower case.
55 ///
56 /// After trimming, all words must be the same length, else this returns an error of type
57 /// [`WordleError::WordLength`].
58 ///
59 /// ```no_run
60 /// use std::fs::File;
61 /// use std::io;
62 /// use rs_wordle_solver::WordBank;
63 /// # use rs_wordle_solver::WordleError;
64 ///
65 /// let words_reader = io::BufReader::new(File::open("path/to/my/words.txt")?);
66 /// let word_bank = WordBank::from_reader(words_reader)?;
67 /// # Ok::<(), WordleError>(())
68 /// ```
69 pub fn from_reader<R: io::BufRead>(word_reader: R) -> Result<Self, WordleError> {
70 let mut word_length = 0;
71 let all_words = word_reader
72 .lines()
73 .filter_map(|maybe_word| {
74 maybe_word.map_or_else(
75 |err| Some(Err(WordleError::from(err))),
76 |word| {
77 let normalized: Option<Result<Arc<str>, WordleError>>;
78 (word_length, normalized) =
79 WordBank::parse_word_to_arc(word_length, word.as_ref());
80 normalized
81 },
82 )
83 })
84 .collect::<Result<Vec<Arc<str>>, WordleError>>()?;
85 Ok(WordBank {
86 all_words,
87 word_length,
88 })
89 }
90
91 /// Constructs a new `WordBank` struct using the words from the given vector. Each word will be
92 /// trimmed and converted to lower case.
93 ///
94 /// After trimming, all words must be the same length, else this returns an error of type
95 /// [`WordleError::WordLength`].
96 ///
97 /// ```
98 /// use std::sync::Arc;
99 /// use rs_wordle_solver::WordBank;
100 /// # use rs_wordle_solver::WordleError;
101 ///
102 /// let words = vec!["abc".to_string(), "DEF ".to_string()];
103 /// let word_bank = WordBank::from_iterator(words.iter())?;
104 ///
105 /// assert_eq!(&word_bank as &[Arc<str>], &[Arc::from("abc"), Arc::from("def")]);
106 /// # Ok::<(), WordleError>(())
107 /// ```
108 pub fn from_iterator<S>(words: impl IntoIterator<Item = S>) -> Result<Self, WordleError>
109 where
110 S: AsRef<str>,
111 {
112 let mut word_length = 0;
113 Ok(WordBank {
114 all_words: words
115 .into_iter()
116 .filter_map(|word| {
117 let normalized: Option<Result<Arc<str>, WordleError>>;
118 (word_length, normalized) =
119 WordBank::parse_word_to_arc(word_length, word.as_ref());
120 normalized
121 })
122 .collect::<Result<Vec<Arc<str>>, WordleError>>()?,
123 word_length,
124 })
125 }
126
127 /// Cleans and parses the given word to an `Arc<str>`, while filtering out empty lines and
128 /// returning an error if the word's length differs from `word_length` (if non-zero).
129 ///
130 /// Returns the new `word_length` to use (if `word_length` was zero before), and the parsed
131 /// word.
132 fn parse_word_to_arc(
133 word_length: usize,
134 word: &str,
135 ) -> (usize, Option<Result<Arc<str>, WordleError>>) {
136 let normalized: Arc<str> = Arc::from(word.trim().to_lowercase().as_str());
137 let this_word_length = normalized.len();
138 if this_word_length == 0 {
139 return (word_length, None);
140 }
141 if word_length != 0 && word_length != this_word_length {
142 return (word_length, Some(Err(WordleError::WordLength(word_length))));
143 }
144 (this_word_length, Some(Ok(normalized)))
145 }
146
147 /// Returns the number of possible words.
148 #[inline]
149 pub fn len(&self) -> usize {
150 self.all_words.len()
151 }
152
153 /// Returns true iff this word bank is empty.
154 #[inline]
155 pub fn is_empty(&self) -> bool {
156 self.all_words.is_empty()
157 }
158
159 /// Returns the length of each word in the word bank.
160 #[inline]
161 pub fn word_length(&self) -> usize {
162 self.word_length
163 }
164}
165
166impl Deref for WordBank {
167 type Target = [Arc<str>];
168
169 /// Derefs the list of words in the `WordBank` as a slice.
170 #[inline]
171 fn deref(&self) -> &Self::Target {
172 &self.all_words
173 }
174}
175
176/// Counts the number of words that contain each letter anywhere, as well as by the location of
177/// each letter.
178///
179/// If you need to know what those words are, see [`WordTracker`].
180///
181/// Use:
182///
183/// ```
184/// # use rs_wordle_solver::details::WordCounter;
185/// # use rs_wordle_solver::details::LocatedLetter;
186/// let all_words = vec!["aba", "bbd", "efg"];
187/// let counter = WordCounter::new(&all_words);
188///
189/// assert_eq!(counter.num_words(), 3);
190/// assert_eq!(counter.num_words_with_letter('b'), 2);
191/// assert_eq!(counter.num_words_with_located_letter(
192/// &LocatedLetter::new('b', 0)), 1);
193/// ```
194#[derive(Clone, Debug)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
196pub struct WordCounter {
197 num_words: u32,
198 num_words_by_ll: HashMap<LocatedLetter, u32>,
199 num_words_by_letter: HashMap<char, u32>,
200}
201
202impl WordCounter {
203 /// Creates a new word counter based on the given word list.
204 #[inline]
205 pub fn new<S>(words: &[S]) -> WordCounter
206 where
207 S: AsRef<str>,
208 {
209 WordCounter::from_iter(words)
210 }
211
212 /// Retrieves the count of words with the given letter at the given location.
213 ///
214 /// ```
215 /// use rs_wordle_solver::details::WordCounter;
216 /// use rs_wordle_solver::details::LocatedLetter;
217 ///
218 /// let all_words = vec!["aba", "bbd", "efg"];
219 /// let counter = WordCounter::from_iter(&all_words);
220 ///
221 /// assert_eq!(counter.num_words_with_located_letter(
222 /// &LocatedLetter::new('b', 0)), 1);
223 /// assert_eq!(counter.num_words_with_located_letter(
224 /// &LocatedLetter::new('b', 1)), 2);
225 /// assert_eq!(counter.num_words_with_located_letter(
226 /// &LocatedLetter::new('b', 2)), 0);
227 /// assert_eq!(counter.num_words_with_located_letter(
228 /// &LocatedLetter::new('b', 3)), 0);
229 /// assert_eq!(counter.num_words_with_located_letter(
230 /// &LocatedLetter::new('z', 0)), 0);
231 /// ```
232 pub fn num_words_with_located_letter(&self, ll: &LocatedLetter) -> u32 {
233 *self.num_words_by_ll.get(ll).unwrap_or(&0)
234 }
235
236 /// Retrieves the count of words that contain the given letter.
237 ///
238 /// ```
239 /// use rs_wordle_solver::details::WordCounter;
240 ///
241 /// let all_words = vec!["aba", "bbd", "efg"];
242 /// let counter = WordCounter::from_iter(&all_words);
243 ///
244 /// assert_eq!(counter.num_words_with_letter('a'), 1);
245 /// assert_eq!(counter.num_words_with_letter('b'), 2);
246 /// assert_eq!(counter.num_words_with_letter('z'), 0);
247 /// ```
248 pub fn num_words_with_letter(&self, letter: char) -> u32 {
249 *self.num_words_by_letter.get(&letter).unwrap_or(&0)
250 }
251
252 /// Retrieves the total number of words in this counter.
253 #[inline]
254 pub fn num_words(&self) -> u32 {
255 self.num_words
256 }
257}
258
259impl<S> FromIterator<S> for WordCounter
260where
261 S: AsRef<str>,
262{
263 /// Creates a new word counter based on the given word list.
264 ///
265 /// ```
266 /// use rs_wordle_solver::details::WordCounter;
267 ///
268 /// let all_words = vec!["bba", "bcd", "efg"];
269 /// let counter: WordCounter = all_words.iter().collect();
270 ///
271 /// assert_eq!(counter.num_words(), 3);
272 /// ```
273 fn from_iter<T>(iter: T) -> Self
274 where
275 T: IntoIterator<Item = S>,
276 {
277 let mut num_words_by_ll: HashMap<LocatedLetter, u32> = HashMap::new();
278 let mut num_words_by_letter: HashMap<char, u32> = HashMap::new();
279 let mut num_words = 0;
280 for word in iter.into_iter() {
281 num_words += 1;
282 for (index, letter) in word.as_ref().char_indices() {
283 *num_words_by_ll
284 .entry(LocatedLetter::new(letter, index as u8))
285 .or_insert(0) += 1;
286 if index == 0
287 || word
288 .as_ref()
289 .chars()
290 .take(index)
291 .all(|other_letter| other_letter != letter)
292 {
293 *num_words_by_letter.entry(letter).or_insert(0) += 1;
294 }
295 }
296 }
297 WordCounter {
298 num_words,
299 num_words_by_ll,
300 num_words_by_letter,
301 }
302 }
303}
304
305/// Computes the unique set of words that contain each letter anywhere, as well as by the location
306/// of each letter.
307///
308/// If you only need to know the number of words instead of the list of words, see [`WordCounter`].
309///
310/// ```
311/// use std::sync::Arc;
312/// use rs_wordle_solver::details::WordTracker;
313/// use rs_wordle_solver::details::LocatedLetter;
314///
315/// let all_words = [Arc::from("aba"), Arc::from("bcd"), Arc::from("efg")];
316/// let tracker = WordTracker::new(&all_words);
317///
318/// assert_eq!(tracker.all_words(), &all_words);
319/// assert_eq!(
320/// Vec::from_iter(tracker.words_with_letter('b')),
321/// vec![&Arc::from("aba"), &Arc::from("bcd")]);
322/// assert_eq!(
323/// Vec::from_iter(tracker.words_with_located_letter(LocatedLetter::new('b', 1))),
324/// vec![&Arc::from("aba")]);
325/// ```
326#[derive(Clone, Debug)]
327pub struct WordTracker<'w> {
328 all_words: &'w [Arc<str>],
329 words_by_letter: HashMap<char, Vec<Arc<str>>>,
330 words_by_located_letter: HashMap<LocatedLetter, Vec<Arc<str>>>,
331}
332
333impl<'w> WordTracker<'w> {
334 /// Constructs a new `WordTracker` from the given words. Note that the words are not checked
335 /// for uniqueness, so if duplicates exist in the given words, then those duplicates will
336 /// remain part of this tracker's information.
337 ///
338 /// ```
339 /// use std::sync::Arc;
340 /// use rs_wordle_solver::details::WordTracker;
341 ///
342 /// let all_words = vec![Arc::from("aba"), Arc::from("bcd"), Arc::from("efg")];
343 /// let tracker = WordTracker::new(&all_words);
344 ///
345 /// assert_eq!(tracker.all_words(), &all_words);
346 /// ```
347 pub fn new<'w_in: 'w>(all_words: &'w_in [Arc<str>]) -> WordTracker<'w> {
348 let mut words_by_letter: HashMap<char, Vec<Arc<str>>> = HashMap::new();
349 let mut words_by_located_letter: HashMap<LocatedLetter, Vec<Arc<str>>> = HashMap::new();
350 for word in all_words.iter() {
351 let word_ref = word.as_ref();
352 for (index, letter) in word_ref.char_indices() {
353 words_by_located_letter
354 .entry(LocatedLetter::new(letter, index as u8))
355 .or_default()
356 .push(Arc::clone(word));
357 if index == 0
358 || word_ref
359 .chars()
360 .take(index)
361 .all(|other_letter| letter != other_letter)
362 {
363 words_by_letter
364 .entry(letter)
365 .or_default()
366 .push(Arc::clone(word));
367 }
368 }
369 }
370 WordTracker {
371 all_words,
372 words_by_letter,
373 words_by_located_letter,
374 }
375 }
376
377 /// Retrieves the full list of words stored in this word tracker.
378 ///
379 /// ```
380 /// use std::sync::Arc;
381 /// use rs_wordle_solver::details::WordTracker;
382 ///
383 /// let all_words = [Arc::from("aba"), Arc::from("bcd"), Arc::from("efg")];
384 /// let tracker = WordTracker::new(&all_words);
385 ///
386 /// assert_eq!(tracker.all_words(), &all_words);
387 /// ```
388 #[inline]
389 pub fn all_words(&self) -> &[Arc<str>] {
390 self.all_words
391 }
392
393 /// Returns true iff any of the words in this tracker contain the given letter.
394 ///
395 /// ```
396 /// use std::sync::Arc;
397 /// use rs_wordle_solver::details::WordTracker;
398 ///
399 /// let all_words = [Arc::from("aba"), Arc::from("bcd"), Arc::from("efg")];
400 /// let tracker = WordTracker::new(&all_words);
401 ///
402 /// assert!(tracker.has_letter('a'));
403 /// assert!(!tracker.has_letter('z'));
404 /// ```
405 #[inline]
406 pub fn has_letter(&self, letter: char) -> bool {
407 self.words_by_letter.contains_key(&letter)
408 }
409
410 /// Returns an [`Iterator`] over words that have the given letter at the given location.
411 ///
412 /// ```
413 /// use std::sync::Arc;
414 /// use rs_wordle_solver::details::WordTracker;
415 /// use rs_wordle_solver::details::LocatedLetter;
416 ///
417 /// let all_words = [Arc::from("bba"), Arc::from("bcd"), Arc::from("efg")];
418 /// let tracker = WordTracker::new(&all_words);
419 ///
420 /// assert_eq!(
421 /// Vec::from_iter(tracker.words_with_located_letter(LocatedLetter::new('b', 0))),
422 /// vec![&Arc::from("bba"), &Arc::from("bcd")]);
423 /// assert_eq!(
424 /// Vec::from_iter(tracker.words_with_located_letter(LocatedLetter::new('b', 1))),
425 /// vec![&Arc::from("bba")]);
426 /// assert_eq!(
427 /// tracker.words_with_located_letter(LocatedLetter::new('z', 1)).count(),
428 /// 0);
429 /// ```
430 pub fn words_with_located_letter(&self, ll: LocatedLetter) -> impl Iterator<Item = &Arc<str>> {
431 self.words_by_located_letter
432 .get(&ll)
433 .map(|words| words.iter())
434 .unwrap_or_default()
435 }
436
437 /// Returns an [`Iterator`] over words that have the given letter.
438 ///
439 /// ```
440 /// use std::sync::Arc;
441 /// use rs_wordle_solver::details::WordTracker;
442 ///
443 /// let all_words = [Arc::from("bba"), Arc::from("bcd"), Arc::from("efg")];
444 /// let tracker = WordTracker::new(&all_words);
445 ///
446 /// assert_eq!(
447 /// Vec::from_iter(tracker.words_with_letter('b')),
448 /// vec![&Arc::from("bba"), &Arc::from("bcd")]);
449 /// assert_eq!(
450 /// Vec::from_iter(tracker.words_with_letter('e')),
451 /// vec![&Arc::from("efg")]);
452 /// assert_eq!(
453 /// tracker.words_with_letter('z').count(),
454 /// 0);
455 /// ```
456 pub fn words_with_letter(&self, letter: char) -> impl Iterator<Item = &Arc<str>> {
457 self.words_by_letter
458 .get(&letter)
459 .map(|words| words.iter())
460 .unwrap_or_default()
461 }
462
463 /// Returns an [`Iterator`] over words that have the given letter, but not at the given
464 /// location.
465 ///
466 /// ```
467 /// use std::sync::Arc;
468 /// use rs_wordle_solver::details::WordTracker;
469 /// use rs_wordle_solver::details::LocatedLetter;
470 ///
471 /// let all_words = [Arc::from("bba"), Arc::from("bcd"), Arc::from("efg")];
472 /// let tracker = WordTracker::new(&all_words);
473 ///
474 /// assert_eq!(
475 /// Vec::from_iter(tracker.words_with_letter_not_here(LocatedLetter::new('b', 1))),
476 /// vec![&Arc::from("bcd")]);
477 /// assert_eq!(
478 /// tracker.words_with_letter_not_here(LocatedLetter::new('b', 0)).count(),
479 /// 0);
480 /// assert_eq!(
481 /// tracker.words_with_letter_not_here(LocatedLetter::new('z', 0)).count(),
482 /// 0);
483 /// ```
484 pub fn words_with_letter_not_here(&self, ll: LocatedLetter) -> impl Iterator<Item = &Arc<str>> {
485 let words_with_letter = self
486 .words_by_letter
487 .get(&ll.letter)
488 .map(|words| words.iter())
489 .unwrap_or_default();
490 words_with_letter
491 .filter(move |&word| word.chars().nth(ll.location as usize).unwrap() != ll.letter)
492 }
493
494 /// Returns an [`Iterator`] over words that don't have the given letter.
495 ///
496 /// ```
497 /// use std::sync::Arc;
498 /// use rs_wordle_solver::details::WordTracker;
499 /// use rs_wordle_solver::details::LocatedLetter;
500 ///
501 /// let all_words = [Arc::from("bba"), Arc::from("bcd"), Arc::from("efg")];
502 /// let tracker = WordTracker::new(&all_words);
503 ///
504 /// assert_eq!(
505 /// Vec::from_iter(tracker.words_without_letter('a')),
506 /// vec![&Arc::from("bcd"), &Arc::from("efg")]);
507 /// assert_eq!(
508 /// Vec::from_iter(tracker.words_without_letter('z')),
509 /// Vec::from_iter(&all_words));
510 /// ```
511 pub fn words_without_letter(&self, letter: char) -> impl Iterator<Item = &'w Arc<str>> {
512 self.all_words
513 .iter()
514 .filter(move |word| !word.contains(letter))
515 }
516}
517
518/// Efficiently tracks all possible words and all unguessed words as zero-cost slices within a
519/// single array of all words.
520#[derive(Clone, Debug)]
521#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
522pub struct GroupedWords {
523 pub all_words: Vec<Arc<str>>,
524 // The `all_words` vector keeps words grouped according to whether they're still possible, and
525 // whether they have been guessed. It's grouped into four sections:
526 // Index: 0
527 // 1. Words that have been guessed, and are still possible. Then index:
528 first_unguessed_possible_word: usize,
529 // 2. Words that have not been guessed, and are still possible. Then index:
530 num_possible_words: usize,
531 // 3. Words that have not been guessed, but are not possible. Then index:
532 first_guessed_impossible_word: usize,
533 // 4. Words that have been guessed, and are not possible.
534}
535
536impl GroupedWords {
537 /// Constructs a new GroupedWords instance. Initially all words are considered possible and
538 /// unguessed.
539 pub fn new(words: WordBank) -> Self {
540 let num_words = words.len();
541 Self {
542 all_words: words.all_words,
543 first_unguessed_possible_word: 0,
544 num_possible_words: num_words,
545 first_guessed_impossible_word: num_words,
546 }
547 }
548
549 pub fn num_possible_words(&self) -> usize {
550 self.num_possible_words
551 }
552
553 pub fn num_unguessed_words(&self) -> usize {
554 self.first_guessed_impossible_word - self.first_unguessed_possible_word
555 }
556
557 /// The slice of all unguessed words. Guaranteed to start with possible words.
558 pub fn unguessed_words(&self) -> &[Arc<str>] {
559 &self.all_words[self.first_unguessed_possible_word..self.first_guessed_impossible_word]
560 }
561
562 /// The slice of all possible words.
563 pub fn possible_words(&self) -> &[Arc<str>] {
564 &self.all_words[0..self.num_possible_words]
565 }
566
567 /// Removes this word from the set of unguessed words, if it's present in the word list.
568 /// This also removes the word from the list of possible words.
569 pub fn remove_guess_if_present(&mut self, guess: &str) {
570 // TODO: Support both by using the start of the array for possible words that have been
571 // guessed.
572 if let Some(position) = self
573 .unguessed_words()
574 .iter()
575 .position(|word| word.as_ref() == guess)
576 .map(|unguessed_position| unguessed_position + self.first_unguessed_possible_word)
577 {
578 // If it's a possible word, put it in section 1.
579 if position < self.num_possible_words {
580 self.all_words
581 .swap(position, self.first_unguessed_possible_word);
582 self.first_unguessed_possible_word += 1;
583 } else {
584 // If it's an impossible word, put it in section 4.
585 self.all_words
586 .swap(position, self.first_guessed_impossible_word - 1);
587 self.first_guessed_impossible_word -= 1;
588 }
589 }
590 }
591
592 /// Filters out possible words for which the filter returns false.
593 pub fn filter_possible_words<F>(&mut self, filter: F)
594 where
595 F: Fn(&str) -> bool,
596 {
597 if self.num_possible_words - self.first_unguessed_possible_word == 0 {
598 return;
599 }
600
601 // Iterate backwards so that, in the common case, we swap the minimum number of words.
602 let mut i = self.num_possible_words - 1;
603 loop {
604 let word = &self.all_words[i];
605
606 if !filter(word.as_ref()) {
607 // Move this word from section 2 (possible unguessed words) to section 3 (impossible
608 // unguessed words).
609 self.num_possible_words -= 1;
610 self.all_words.swap(i, self.num_possible_words);
611 }
612
613 if i == self.first_unguessed_possible_word {
614 break;
615 }
616 i -= 1;
617 }
618 // See if there are any guessed possible words to check. This is rare.
619 if i == 0 {
620 return;
621 }
622
623 i -= 1;
624 loop {
625 let word = &self.all_words[i];
626
627 if !filter(word.as_ref()) {
628 // We're going to bump the word to the end of section 1, then end of section 2, then end of section 3.
629 self.first_guessed_impossible_word -= 1;
630 self.num_possible_words -= 1;
631 self.first_unguessed_possible_word -= 1;
632 self.all_words.swap(i, self.first_unguessed_possible_word);
633 self.all_words
634 .swap(self.first_unguessed_possible_word, self.num_possible_words);
635 self.all_words
636 .swap(self.num_possible_words, self.first_guessed_impossible_word);
637 }
638 if i == 0 {
639 break;
640 }
641 i -= 1;
642 }
643 }
644}
645
646impl Display for GroupedWords {
647 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648 f.write_str("GroupedWords { possible, guessed: ")?;
649 f.debug_list()
650 .entries(self.all_words[0..self.first_unguessed_possible_word].iter())
651 .finish()?;
652 f.write_str("; possible, unguessed: ")?;
653 f.debug_list()
654 .entries(
655 self.all_words[self.first_unguessed_possible_word..self.num_possible_words].iter(),
656 )
657 .finish()?;
658 f.write_str("; impossible, guessed: ")?;
659 f.debug_list()
660 .entries(
661 self.all_words[self.first_guessed_impossible_word..self.all_words.len()].iter(),
662 )
663 .finish()?;
664 f.write_str("; impossible, unguessed: ")?;
665 f.debug_list()
666 .entries(
667 self.all_words[self.num_possible_words..self.first_guessed_impossible_word].iter(),
668 )
669 .finish()?;
670 f.write_str(" }")
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use std::collections::HashSet;
678
679 #[test]
680 fn test_grouped_words_new() -> Result<(), WordleError> {
681 let words =
682 WordBank::from_iterator(&[Arc::from("the"), Arc::from("big"), Arc::from("dog")])?;
683 let grouped_words = GroupedWords::new(words.clone());
684
685 assert_eq!(grouped_words.all_words, words.to_vec());
686 assert_eq!(grouped_words.num_possible_words(), 3);
687 assert_eq!(grouped_words.num_unguessed_words(), 3);
688
689 Ok(())
690 }
691
692 #[test]
693 fn test_grouped_words_remove_guess_if_present() -> Result<(), WordleError> {
694 let words =
695 WordBank::from_iterator(&[Arc::from("the"), Arc::from("big"), Arc::from("dog")])?;
696 let mut grouped_words = GroupedWords::new(words.clone());
697
698 // Remove random word, should be no-op.
699 grouped_words.remove_guess_if_present("cat");
700
701 assert_eq!(grouped_words.num_possible_words(), 3);
702 assert_eq!(grouped_words.num_unguessed_words(), 3);
703 assert_eq!(grouped_words.unguessed_words(), words.to_vec());
704
705 // Remove a present word, should remove it from unguessed words.
706 grouped_words.remove_guess_if_present("big");
707
708 assert_eq!(grouped_words.num_possible_words(), 3);
709 assert_eq!(grouped_words.num_unguessed_words(), 2);
710 assert_eq!(
711 grouped_words.unguessed_words(),
712 &[Arc::from("the"), Arc::from("dog")]
713 );
714 assert_eq!(
715 HashSet::from_iter(grouped_words.possible_words()),
716 words.iter().collect::<HashSet<_>>()
717 );
718
719 // Remove again, should be no-op.
720 grouped_words.remove_guess_if_present("big");
721
722 assert_eq!(grouped_words.num_possible_words(), 3);
723 assert_eq!(grouped_words.num_unguessed_words(), 2);
724 assert_eq!(
725 grouped_words.unguessed_words(),
726 &[Arc::from("the"), Arc::from("dog")]
727 );
728
729 Ok(())
730 }
731
732 #[test]
733 fn test_grouped_words_filter_possible_words() -> Result<(), WordleError> {
734 let words =
735 WordBank::from_iterator(&[Arc::from("the"), Arc::from("big"), Arc::from("dog")])?;
736 let mut grouped_words = GroupedWords::new(words.clone());
737
738 grouped_words.filter_possible_words(|word| word == "big");
739
740 assert_eq!(grouped_words.num_possible_words(), 1);
741 assert_eq!(grouped_words.num_unguessed_words(), 3);
742 assert_eq!(grouped_words.possible_words(), &[Arc::from("big")]);
743 assert_eq!(
744 HashSet::from_iter(grouped_words.unguessed_words()),
745 words.iter().collect::<HashSet<_>>()
746 );
747
748 Ok(())
749 }
750
751 #[test]
752 fn test_grouped_words_guessing_the_word() -> Result<(), WordleError> {
753 let words =
754 WordBank::from_iterator(&[Arc::from("the"), Arc::from("big"), Arc::from("dog")])?;
755 let mut grouped_words = GroupedWords::new(words.clone());
756
757 grouped_words.remove_guess_if_present("big");
758 grouped_words.filter_possible_words(|word| word == "big");
759
760 assert_eq!(grouped_words.num_possible_words(), 1);
761 assert_eq!(grouped_words.num_unguessed_words(), 2);
762 assert_eq!(grouped_words.possible_words(), &[Arc::from("big")]);
763 assert_eq!(
764 grouped_words
765 .unguessed_words()
766 .iter()
767 .collect::<HashSet<_>>(),
768 HashSet::from_iter(&[Arc::from("the"), Arc::from("dog")])
769 );
770
771 Ok(())
772 }
773
774 #[test]
775 fn test_grouped_words_display() -> Result<(), WordleError> {
776 let words = WordBank::from_iterator(&[
777 Arc::from("the"),
778 Arc::from("big"),
779 Arc::from("dog"),
780 Arc::from("cat"),
781 Arc::from("bat"),
782 ])?;
783 let mut grouped_words = GroupedWords::new(words.clone());
784
785 grouped_words.remove_guess_if_present("the");
786 grouped_words.filter_possible_words(|word| word == "big" || word == "bat");
787 grouped_words.remove_guess_if_present("big");
788
789 assert_eq!(
790 format!("{}", grouped_words),
791 "GroupedWords { possible, guessed: [\"big\"]; possible, unguessed: [\"bat\"]; impossible, guessed: [\"the\"]; impossible, unguessed: [\"cat\", \"dog\"] }"
792 );
793 Ok(())
794 }
795}