smart_read/
list_constraints.rs

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