hangman_solver_lib/solver/
hangman_result.rs

1// SPDX-License-Identifier: EUPL-1.2
2
3use cfg_if::cfg_if;
4use std::fmt::Display;
5
6use crate::Language;
7use crate::solver::infallible_char_collection::InfallibleCharCollection as _;
8
9#[cfg(feature = "pyo3")]
10use pyo3::prelude::*;
11
12#[cfg(feature = "wasm-bindgen")]
13use js_sys::JsString;
14#[cfg(feature = "wasm-bindgen")]
15use wasm_bindgen::prelude::*;
16
17#[inline]
18fn join_with_max_length<T: ExactSizeIterator<Item = String>>(
19    strings: T,
20    sep: &str,
21    max_len: usize,
22) -> impl Display {
23    let last_index = strings.len() - 1;
24    let mut string = String::with_capacity(max_len);
25    for (i, item) in strings.enumerate() {
26        let current_sep = if i == 0 { "" } else { sep };
27        let min_next_len = if i == last_index { 0 } else { sep.len() + 3 };
28        if string.char_count()
29            + current_sep.len()
30            + item.char_count()
31            + min_next_len
32            > max_len
33        {
34            string.extend([current_sep, "..."]);
35            break;
36        }
37        string.extend([current_sep, &item]);
38    }
39    debug_assert!(string.char_count() <= max_len);
40    string
41}
42
43cfg_if! {
44    if #[cfg(feature = "pyo3")] {
45        #[pyclass]
46        pub struct HangmanResult {
47            #[pyo3(get)]
48            pub input: String,
49            #[pyo3(get)]
50            pub matching_words_count: u32,
51            #[pyo3(get)]
52            pub invalid: Vec<char>,
53            #[pyo3(get, name = "words")]
54            pub possible_words: Vec<&'static str>,
55            #[pyo3(get)]
56            pub language: Language,
57            #[pyo3(get)]
58            pub letter_frequency: Vec<(char, u32)>,
59        }
60
61        #[pymethods]
62        impl HangmanResult {
63            fn __repr__(&self) -> String {
64                let id = self as *const Self;
65                let count = self.matching_words_count;
66                let lang = self.language.name();
67                let pattern = &self.input;
68                let invalid = &self.invalid;
69
70                if let Some(word) = (count == 1).then_some(()).and_then(|()| self.possible_words.first()) {
71                    format!("<HangmanResult lang={lang} pattern={pattern} invalid={invalid:?} count={count} word={word} at {id:?}>")
72                } else if count == 1 {
73                    let letters: Box<[char]> = self.letter_frequency.iter().map(|(ch, _)| *ch).collect();
74                    format!("<HangmanResult lang={lang} pattern={pattern} invalid={invalid:?} count={count} letters={letters:?} at {id:?}>")
75                } else if let Some(mcl) = self.letter_frequency.first().map(|(ch, _)| ch) {
76                    format!("<HangmanResult lang={lang} pattern={pattern} invalid={invalid:?} count={count} guess={mcl} at {id:?}>")
77                } else {
78                    format!("<HangmanResult lang={lang} pattern={pattern} invalid={invalid:?} count={count} at {id:?}>")
79                }
80            }
81        }
82    } else {
83        pub struct HangmanResult {
84            pub input: String,
85            pub invalid: Vec<char>,
86            pub matching_words_count: u32,
87            pub possible_words: Vec<&'static str>,
88            pub language: Language,
89            pub letter_frequency: Vec<(char, u32)>,
90        }
91    }
92}
93
94impl std::fmt::Display for HangmanResult {
95    fn fmt(&self, file: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        let max_line_length: usize = file.width().unwrap_or(80);
97        let invalid: String = self.invalid.iter().collect();
98        write!(
99            file,
100            "Found {} words (input: {}, invalid: {})",
101            self.matching_words_count, self.input, invalid,
102        )?;
103        if self.possible_words.is_empty() {
104            return Ok(());
105        }
106        writeln!(file)?;
107        write!(
108            file,
109            " words:   {}",
110            join_with_max_length(
111                self.possible_words.iter().map(|word| String::from(*word)),
112                ", ",
113                max_line_length - " words:   ".len(),
114            )
115        )?;
116
117        if !self.letter_frequency.is_empty() {
118            writeln!(file)?;
119            write!(
120                file,
121                " letters: {}",
122                join_with_max_length(
123                    self.letter_frequency
124                        .iter()
125                        .map(|(ch, f)| format!("{ch}: {f}")),
126                    ", ",
127                    max_line_length - " letters: ".len(),
128                )
129            )?;
130        }
131        Ok(())
132    }
133}
134
135#[cfg(feature = "wasm-bindgen")]
136#[wasm_bindgen(getter_with_clone)]
137pub struct WasmHangmanResult {
138    #[wasm_bindgen(readonly)]
139    pub input: JsString,
140    #[wasm_bindgen(readonly)]
141    pub invalid: JsString,
142    #[wasm_bindgen(readonly)]
143    pub matching_words_count: u32,
144    #[wasm_bindgen(readonly)]
145    pub possible_words: Vec<JsString>,
146    #[wasm_bindgen(readonly)]
147    pub letter_frequency: JsString,
148}