smart_read/
list_constraints.rs

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