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}