diff options
Diffstat (limited to 'src/choice/mod.rs')
-rw-r--r-- | src/choice/mod.rs | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/src/choice/mod.rs b/src/choice/mod.rs new file mode 100644 index 0000000..4a4e88f --- /dev/null +++ b/src/choice/mod.rs @@ -0,0 +1,267 @@ +use std::convert::TryInto; + +use crate::config::Config; +use crate::error::Error; +use crate::result::Result; +use crate::writeable::Writeable; +use crate::writer::WriteReceiver; + +#[cfg(test)] +mod test; + +#[derive(Debug)] +pub struct Choice { + pub start: isize, + pub end: isize, + pub kind: ChoiceKind, + negative_index: bool, + reversed: bool, +} + +#[derive(Debug, PartialEq)] +pub enum ChoiceKind { + Single, + RustExclusiveRange, + RustInclusiveRange, + ColonRange, +} + +impl Choice { + pub fn new(start: isize, end: isize, kind: ChoiceKind) -> Self { + let negative_index = start < 0 || end < 0; + let reversed = end < start && !(start >= 0 && end < 0); + Choice { + start, + end, + kind, + negative_index, + reversed, + } + } + + pub fn print_choice<W: WriteReceiver>( + &self, + line: &str, + config: &Config, + handle: &mut W, + ) -> Result<()> { + if config.opt.character_wise { + self.print_choice_generic(line.chars(), config, handle) + } else { + let line_iter = config + .separator + .split(line) + .filter(|s| !s.is_empty() || config.opt.non_greedy); + self.print_choice_generic(line_iter, config, handle) + } + } + + pub fn is_reverse_range(&self) -> bool { + self.reversed + } + + pub fn has_negative_index(&self) -> bool { + self.negative_index + } + + fn print_choice_generic<W, T, I>( + &self, + mut iter: I, + config: &Config, + handle: &mut W, + ) -> Result<()> + where + W: WriteReceiver, + T: Writeable, + I: Iterator<Item = T>, + { + if self.is_reverse_range() && !self.has_negative_index() { + self.print_choice_reverse(iter, config, handle)?; + } else if self.has_negative_index() { + self.print_choice_negative(iter, config, handle)?; + } else { + if self.start > 0 { + iter.nth((self.start - 1).try_into()?); + } + let range = self + .end + .checked_sub(self.start) + .ok_or_else(|| Error::Config("expected end > start".into()))?; + Choice::print_choice_loop_max_items(iter, config, handle, range)?; + } + + Ok(()) + } + + fn print_choice_loop_max_items<W, T, I>( + iter: I, + config: &Config, + handle: &mut W, + max_items: isize, + ) -> Result<()> + where + W: WriteReceiver, + T: Writeable, + I: Iterator<Item = T>, + { + let mut peek_iter = iter.peekable(); + for i in 0..=max_items { + match peek_iter.next() { + Some(s) => { + handle.write_choice(s, config, peek_iter.peek().is_some() && i != max_items)?; + } + None => break, + }; + } + + Ok(()) + } + + /// Print choices that include at least one negative index + fn print_choice_negative<W, T, I>(&self, iter: I, config: &Config, handle: &mut W) -> Result<()> + where + W: WriteReceiver, + T: Writeable, + I: Iterator<Item = T>, + { + let vec = iter.collect::<Vec<_>>(); + + if let Some((start, end)) = self.get_negative_start_end(&vec)? { + if end > start { + for word in vec[start..std::cmp::min(end, vec.len() - 1)].iter() { + handle.write_choice(*word, config, true)?; + } + handle.write_choice(vec[std::cmp::min(end, vec.len() - 1)], config, false)?; + } else if self.start < 0 { + for word in vec[end + 1..=std::cmp::min(start, vec.len() - 1)] + .iter() + .rev() + { + handle.write_choice(*word, config, true)?; + } + handle.write_choice(vec[end], config, false)?; + } + } + + Ok(()) + } + + fn print_choice_reverse<W, T, I>( + &self, + mut iter: I, + config: &Config, + handle: &mut W, + ) -> Result<()> + where + W: WriteReceiver, + T: Writeable, + I: Iterator<Item = T>, + { + if self.end > 0 { + iter.nth((self.end - 1).try_into()?); + } + + let mut stack = Vec::new(); + for i in 0..=(self.start - self.end) { + match iter.next() { + Some(s) => stack.push(s), + None => break, + } + + if self.start <= self.end + i { + break; + } + } + + let mut peek_iter = stack.iter().rev().peekable(); + while let Some(s) = peek_iter.next() { + handle.write_choice(*s, config, peek_iter.peek().is_some())?; + } + + Ok(()) + } + + /// Get the absolute indexes of a choice range based on the slice length + /// + /// N.B. that this assumes that at least one index is negative - do not try to call this + /// function with a purely positive range. + /// + /// Returns Ok(None) if the resulting choice range would not include any item in the slice. + fn get_negative_start_end<T>(&self, slice: &[T]) -> Result<Option<(usize, usize)>> { + if slice.len() == 0 { + return Ok(None); + } + + let start_abs = self.start.checked_abs().ok_or_else(|| { + Error::Config(format!( + "Minimum index value supported is isize::MIN + 1 ({})", + isize::MIN + 1 + )) + })?; + + let slice_len_as_isize = slice.len().try_into()?; + + if self.kind == ChoiceKind::Single { + if start_abs <= slice_len_as_isize { + let idx = (slice_len_as_isize - start_abs).try_into()?; + Ok(Some((idx, idx))) + } else { + Ok(None) + } + } else { + let end_abs = self.end.checked_abs().ok_or_else(|| { + Error::Config(format!( + "Minimum index value supported is isize::MIN + 1 ({})", + isize::MIN + 1 + )) + })?; + + if self.start >= 0 { + // then we assume self.end is negative + let start = self.start.try_into()?; + + if end_abs <= slice_len_as_isize + || start <= slice.len() + || (end_abs > slice_len_as_isize && start > slice.len()) + { + let end = slice.len().saturating_sub(end_abs.try_into()?); + Ok(Some(( + std::cmp::min(start, slice.len().saturating_sub(1)), + std::cmp::min(end, slice.len().saturating_sub(1)), + ))) + } else { + Ok(None) + } + } else if self.end >= 0 { + // then we assume self.start is negative + let end = self.end.try_into()?; + + if start_abs <= slice_len_as_isize + || end <= slice.len() + || (start_abs > slice_len_as_isize && end > slice.len()) + { + let start = slice.len().saturating_sub(start_abs.try_into()?); + Ok(Some(( + std::cmp::min(start, slice.len().saturating_sub(1)), + std::cmp::min(end, slice.len().saturating_sub(1)), + ))) + } else { + Ok(None) + } + } else { + // both indices are negative + let start = slice.len().saturating_sub(start_abs.try_into()?); + let end = slice.len().saturating_sub(end_abs.try_into()?); + + if start_abs <= slice_len_as_isize || end_abs <= slice_len_as_isize { + Ok(Some(( + std::cmp::min(start, slice.len().saturating_sub(1)), + std::cmp::min(end, slice.len().saturating_sub(1)), + ))) + } else { + Ok(None) + } + } + } + } +} |