lib_wpsr/
words.rs

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        // Setup settings
40        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}