smart_read/
list_constraints.rs

1use crate::*;
2use std::collections::{LinkedList, VecDeque};
3
4
5
6fn read_list<Data>(input_options: &[InputOption<Data>], prompt: Option<String>, default: Option<usize>) -> BoxResult<usize> {
7	if input_options.is_empty() {return Err(Box::new(ListConstraintError::EmptyList));}
8	
9	// get prompt data
10	let prompt = prompt.unwrap_or(String::from("Enter one of the following:"));
11	let display_strings =
12		input_options.iter().enumerate()
13		.map(|(i, option)| {
14			option.get_display_string(default.map(|default| i == default))
15		})
16		.collect::<Vec<_>>();
17	
18	// lists for string to match against, which option that string goes with, and whether that string is an alt_name
19	let (mut all_choose_strings, mut choose_name_mappings, mut choose_name_hidden_flags) = (vec!(), vec!(), vec!());
20	for (i, option) in input_options.iter().enumerate() {
21		if let Some(bulletin_string) = option.bulletin_string.as_deref() {
22			all_choose_strings.push(bulletin_string);
23			choose_name_mappings.push(i);
24			choose_name_hidden_flags.push(false);
25		}
26		all_choose_strings.push(&*option.main_name);
27		choose_name_mappings.push(i);
28		choose_name_hidden_flags.push(false);
29		for alt_name in &option.alt_names {
30			all_choose_strings.push(alt_name);
31			choose_name_mappings.push(i);
32			choose_name_hidden_flags.push(true);
33		}
34	}
35	
36	// misc work
37	let print_prompt = || {
38		println!("{prompt}");
39		for option in display_strings.iter() {
40			println!("{option}");
41		}
42		println!();
43	};
44	
45	if input_options.len() == 1 {
46		print_prompt();
47		println!();
48		println!("Automatically choosing the first option because it is the only option");
49		return Ok(0);
50	}
51	
52	print_prompt();
53	let mut input = read_stdin()?;
54	
55	// read input
56	loop {
57		if input.is_empty() && let Some(default) = default {
58			return Ok(default);
59		}
60		
61		// find exact match
62		for (i, option) in all_choose_strings.iter().enumerate() {
63			if option.eq_ignore_ascii_case(&input) {
64				let chosen_index = choose_name_mappings[i];
65				return Ok(chosen_index);
66			}
67		}
68		
69		println!();
70		println!("Invalid option.");
71		
72		// try fuzzy match
73		if let Some(possible_choose_string_index) = custom_fuzzy_search(&input, &all_choose_strings) {
74			let possible_option_index = choose_name_mappings[possible_choose_string_index];
75			let possible_option = &input_options[possible_option_index];
76			if choose_name_hidden_flags[possible_choose_string_index] {
77				print!("Did you mean to type \"{}\", for option \"{}\"? (enter nothing to confirm, or re-enter input) ", all_choose_strings[possible_choose_string_index], possible_option.main_name);
78			} else {
79				print!("Did you mean \"{}\"? (enter nothing to confirm, or re-enter input) ", all_choose_strings[possible_choose_string_index]);
80			}
81			let new_input = read_stdin()?;
82			if new_input.is_empty() {
83				let chosen_index = possible_option_index;
84				return Ok(chosen_index);
85			}
86			input = new_input;
87		} else {
88			print!("Invalid option, please re-enter input: ");
89			input = read_stdin()?;
90		}
91		
92	}
93}
94
95
96
97impl<'a, Data> TryRead for &'a [InputOption<Data>] {
98	type Output = (usize, &'a InputOption<Data>);
99	type Default = usize;
100	fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
101		let chosen_index = read_list(self, prompt, default)?;
102		Ok((chosen_index, &self[chosen_index]))
103	}
104}
105
106impl<Data, const LEN: usize> TryRead for [InputOption<Data>; LEN] {
107	type Output = (usize, InputOption<Data>);
108	type Default = usize;
109	fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
110		let chosen_index = read_list(&self, prompt, default)?;
111		Ok((chosen_index, self.into_iter().nth(chosen_index).expect("chosen index is out of bounds")))
112	}
113}
114
115
116
117/// Error type
118#[derive(Debug)]
119pub enum ListConstraintError {
120	/// This exists because an empty list would be a softlock
121	EmptyList,
122}
123
124impl Error for ListConstraintError {}
125
126impl Display for ListConstraintError {
127	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128		match self {
129			Self::EmptyList => write!(f, "List Constraint is empty"),
130		}
131	}
132}
133
134
135
136/// Custom implementation of fuzzy search, returns the index of the closest match
137pub fn custom_fuzzy_search(pattern: &str, items: &[&str]) -> Option<usize> {
138	let (mut best_score, mut best_index) = (custom_fuzzy_match(pattern, items[0]), 0);
139	for (i, item) in items.iter().enumerate().skip(1) {
140		let score = custom_fuzzy_match(pattern, item);
141		if score > best_score {
142			best_score = score;
143			best_index = i;
144		}
145	}
146	if best_score > 0.0 {
147		Some(best_index)
148	} else {
149		None
150	}
151}
152
153/// Custom implementation of fuzzy match. Not efficient at all, but gives good results
154pub fn custom_fuzzy_match(pattern: &str, item: &str) -> f32 {
155	let mut best_score = 0.0f32;
156	let offset_start = pattern.len() as isize * -1 + 1;
157	let offset_end = item.len() as isize - 1;
158	for offset in offset_start..=offset_end {
159		let item_slice = &item[offset.max(0) as usize .. (offset + pattern.len() as isize).min(item.len() as isize) as usize];
160		let pattern_slice = &pattern[(offset * -1).max(0) as usize .. (item.len() as isize - offset).min(pattern.len() as isize) as usize];
161		let mut slice_score = 0.0f32;
162		for (item_char, pattern_char) in item_slice.chars().zip(pattern_slice.chars()) {
163			if item_char.eq_ignore_ascii_case(&pattern_char) {
164				slice_score += 3.;
165			} else {
166				slice_score -= 1.;
167			}
168		}
169		slice_score *= 1. - offset as f32 / item.len() as f32 * 0.5; // give higher value to earlier matches, best weight is at offset = 0
170		best_score = best_score.max(slice_score);
171	}
172	best_score
173}
174
175
176
177
178
179/// Allows you to add more data to an option
180/// 
181/// Example:
182/// 
183/// ```
184/// // example data
185/// let mut colors = vec!("Red", "green", "Blue");
186/// 
187/// // prepare options, only capitalized colors can be removed
188/// let mut option_number = 1;
189/// let choosable_colors =
190/// 	colors.iter().enumerate()
191/// 	.filter_map(|(i, color_name)| {
192/// 		let first_char = color_name.chars().next()?;
193/// 		if first_char.is_lowercase() {return None;}
194/// 		Some(OptionWithData::new(option_number, color_name, vec!(), i))
195/// 	})
196/// 	.collect::<Vec<_>>();
197/// 
198/// // prompt
199/// let OptionWithData {data: index_to_remove, ..} = prompt!("Choose a color to remove: "; choosable_colors);
200/// colors.remove(index_to_remove);
201/// ```
202pub struct InputOption<Data> {
203	/// This is what's displayed before the colon
204	pub bulletin_string: Option<String>,
205	/// This is what's shown as the option name
206	pub main_name: String,
207	/// These are alternate valid strings that the user could enter to choose this option
208	pub alt_names: Vec<String>,
209	/// Extra data for storing whatever you want
210	pub data: Data,
211}
212
213impl<Data> InputOption<Data> {
214	/// Basic initializer
215	pub fn new(bulletin: impl Into<String>, display: impl Into<String>, choose: Vec<impl Into<String>>, data: Data) -> Self {
216		Self {
217			bulletin_string: Some(bulletin.into()),
218			main_name: display.into(),
219			alt_names: choose.into_iter().map(|v| v.into()).collect(),
220			data,
221		}
222	}
223	/// Initializer without bulletin string
224	pub fn new_without_bulletin(display: impl Into<String>, choose: Vec<impl Into<String>>, data: Data) -> Self {
225		Self {
226			bulletin_string: None,
227			main_name: display.into(),
228			alt_names: choose.into_iter().map(|v| v.into()).collect(),
229			data,
230		}
231	}
232	/// Internal function
233	pub fn get_display_string(&self, is_default: Option<bool>) -> String {
234		match (self.bulletin_string.as_deref(), is_default) {
235			(Some(bulletin_string), Some(true )) => format!("[{bulletin_string}]: {}", self.main_name),
236			(Some(bulletin_string), Some(false)) => format!(" {bulletin_string}:  {}", self.main_name),
237			(None                       , Some(true )) => format!("[{}]", self.main_name),
238			(None                       , Some(false)) => format!(" {} ", self.main_name),
239			(Some(bulletin_string), None       ) => format!("{bulletin_string}: {}", self.main_name),
240			(None                       , None       ) => format!("{}", self.main_name),
241		}
242	}
243}
244
245
246
247
248
249impl<'a, T: Display> TryRead for &'a [T] {
250	type Output = (usize, &'a T);
251	type Default = usize;
252	fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
253		let options = self.iter().enumerate()
254			.map(|(i, option)| {
255				InputOption {
256					bulletin_string: Some((i + 1).to_string()),
257					main_name: option.to_string(),
258					alt_names: vec!(),
259					data: (),
260				}
261			})
262			.collect::<Vec<_>>();
263		let chosen_index = (&*options).try_read_line(prompt, default)?.0;
264		Ok((chosen_index, &self[chosen_index]))
265	}
266}
267
268impl<T: Display, const LEN: usize> TryRead for [T; LEN] {
269	type Output = (usize, T);
270	type Default = usize;
271	fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
272		let options = self.iter().enumerate()
273			.map(|(i, option)| {
274				InputOption {
275					bulletin_string: Some((i + 1).to_string()),
276					main_name: option.to_string(),
277					alt_names: vec!(),
278					data: (),
279				}
280			})
281			.collect::<Vec<_>>();
282		let chosen_index = (&*options).try_read_line(prompt, default)?.0;
283		Ok((chosen_index, self.into_iter().nth(chosen_index).expect("chosen index is out of bounds")))
284	}
285}
286
287impl<T: Display> TryRead for Vec<T> {
288	type Output = (usize, T);
289	type Default = usize;
290	fn try_read_line(mut self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
291		let options = self.iter().enumerate()
292			.map(|(i, option)| {
293				InputOption {
294					bulletin_string: Some((i + 1).to_string()),
295					main_name: option.to_string(),
296					alt_names: vec!(),
297					data: (),
298				}
299			})
300			.collect::<Vec<_>>();
301		let chosen_index = (&*options).try_read_line(prompt, default)?.0;
302		Ok((chosen_index, self.swap_remove(chosen_index)))
303	}
304}
305
306impl<T: Display> TryRead for VecDeque<T> {
307	type Output = (usize, T);
308	type Default = usize;
309	fn try_read_line(mut self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
310		let options = self.iter().enumerate()
311			.map(|(i, option)| {
312				InputOption {
313					bulletin_string: Some((i + 1).to_string()),
314					main_name: option.to_string(),
315					alt_names: vec!(),
316					data: (),
317				}
318			})
319			.collect::<Vec<_>>();
320		let chosen_index = (&*options).try_read_line(prompt, default)?.0;
321		Ok((chosen_index, self.swap_remove_back(chosen_index).expect("chosen index is out of bounds")))
322	}
323}
324
325impl<T: Display> TryRead for LinkedList<T> {
326	type Output = (usize, T);
327	type Default = usize;
328	fn try_read_line(self, prompt: Option<String>, default: Option<Self::Default>) -> BoxResult<Self::Output> {
329		let options = self.iter().enumerate()
330			.map(|(i, option)| {
331				InputOption {
332					bulletin_string: Some((i + 1).to_string()),
333					main_name: option.to_string(),
334					alt_names: vec!(),
335					data: (),
336				}
337			})
338			.collect::<Vec<_>>();
339		let chosen_index = (&*options).try_read_line(prompt, default)?.0;
340		Ok((chosen_index, self.into_iter().nth(chosen_index).expect("chosen index is out of bounds")))
341	}
342}