diff --git a/src/choice.rs b/src/choice.rs index 9d50b67..63ddb2e 100644 --- a/src/choice.rs +++ b/src/choice.rs @@ -1,15 +1,26 @@ +use std::convert::TryInto; + use crate::config::Config; use crate::io::{BufWriter, Write}; #[derive(Debug)] pub struct Choice { - pub start: usize, - pub end: usize, + pub start: isize, + pub end: isize, + negative_index: bool, + reversed: bool, } impl Choice { - pub fn new(start: usize, end: usize) -> Self { - Choice { start, end } + pub fn new(start: isize, end: isize) -> Self { + let negative_index = start < 0 || end < 0; + let reversed = end < start; + Choice { + start, + end, + negative_index, + reversed, + } } pub fn print_choice( @@ -20,9 +31,9 @@ impl Choice { ) { let mut line_iter = config.separator.split(line).filter(|s| !s.is_empty()); - if self.is_reverse_range() { + if self.is_reverse_range() && !self.has_negative_index() { if self.end > 0 { - line_iter.nth(self.end - 1); + line_iter.nth((self.end - 1).try_into().unwrap()); } let mut stack = Vec::new(); @@ -43,9 +54,37 @@ impl Choice { None => break, } } + } else if self.has_negative_index() { + let vec = line_iter.collect::>(); + + let start = if self.start >= 0 { + self.start.try_into().unwrap() + } else { + vec.len() + .checked_sub(self.start.abs().try_into().unwrap()) + .unwrap() + }; + + let end = if self.end >= 0 { + self.end.try_into().unwrap() + } else { + vec.len() + .checked_sub(self.end.abs().try_into().unwrap()) + .unwrap() + }; + + if end > start { + for word in vec[start..=std::cmp::min(end, vec.len() - 1)].iter() { + Choice::write_bytes(handle, word.as_bytes()); + } + } else if self.start < 0 { + for word in vec[end..=std::cmp::min(start, vec.len() - 1)].iter().rev() { + Choice::write_bytes(handle, word.as_bytes()); + } + } } else { if self.start > 0 { - line_iter.nth(self.start - 1); + line_iter.nth((self.start - 1).try_into().unwrap()); } for i in 0..=(self.end - self.start) { @@ -73,7 +112,11 @@ impl Choice { } pub fn is_reverse_range(&self) -> bool { - self.end < self.start + self.reversed + } + + pub fn has_negative_index(&self) -> bool { + self.negative_index } } @@ -383,6 +426,93 @@ mod tests { MockStdout::str_from_buf_writer(handle) ); } + + #[test] + fn print_neg3_to_neg1() { + let config = Config::from_iter(vec!["choose", "-3:-1"]); + let mut handle = BufWriter::new(MockStdout::new()); + config.opt.choice[0].print_choice( + &String::from("rust lang is pretty darn cool"), + &config, + &mut handle, + ); + assert_eq!( + String::from("pretty darn cool"), + MockStdout::str_from_buf_writer(handle) + ); + } + + #[test] + fn print_neg1_to_neg3() { + let config = Config::from_iter(vec!["choose", "-1:-3"]); + let mut handle = BufWriter::new(MockStdout::new()); + config.opt.choice[0].print_choice( + &String::from("rust lang is pretty darn cool"), + &config, + &mut handle, + ); + assert_eq!( + String::from("cool darn pretty"), + MockStdout::str_from_buf_writer(handle) + ); + } + + #[test] + fn print_neg2_to_end() { + let config = Config::from_iter(vec!["choose", "-2:"]); + let mut handle = BufWriter::new(MockStdout::new()); + config.opt.choice[0].print_choice( + &String::from("rust lang is pretty darn cool"), + &config, + &mut handle, + ); + assert_eq!( + String::from("darn cool"), + MockStdout::str_from_buf_writer(handle) + ); + } + + #[test] + fn print_start_to_neg3() { + let config = Config::from_iter(vec!["choose", ":-3"]); + let mut handle = BufWriter::new(MockStdout::new()); + config.opt.choice[0].print_choice( + &String::from("rust lang is pretty darn cool"), + &config, + &mut handle, + ); + assert_eq!( + String::from("rust lang is pretty"), + MockStdout::str_from_buf_writer(handle) + ); + } + + #[test] + fn print_1_to_neg3() { + let config = Config::from_iter(vec!["choose", "1:-3"]); + let mut handle = BufWriter::new(MockStdout::new()); + config.opt.choice[0].print_choice( + &String::from("rust lang is pretty darn cool"), + &config, + &mut handle, + ); + assert_eq!( + String::from("lang is pretty"), + MockStdout::str_from_buf_writer(handle) + ); + } + + #[test] + fn print_5_to_neg3_empty() { + let config = Config::from_iter(vec!["choose", "5:-3"]); + let mut handle = BufWriter::new(MockStdout::new()); + config.opt.choice[0].print_choice( + &String::from("rust lang is pretty darn cool"), + &config, + &mut handle, + ); + assert_eq!(String::from(""), MockStdout::str_from_buf_writer(handle)); + } } mod is_reverse_range_tests { diff --git a/src/config.rs b/src/config.rs index c12fc11..11396f5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,7 @@ use crate::choice::Choice; use crate::opt::Opt; lazy_static! { - static ref PARSE_CHOICE_RE: Regex = Regex::new(r"^(\d*):(\d*)$").unwrap(); + static ref PARSE_CHOICE_RE: Regex = Regex::new(r"^(-?\d*):(-?\d*)$").unwrap(); } pub struct Config { @@ -66,7 +66,7 @@ impl Config { }; let start = if cap[1].is_empty() { - usize::min_value() + 0 } else { match cap[1].parse() { Ok(x) => x, @@ -78,7 +78,7 @@ impl Config { }; let end = if cap[2].is_empty() { - usize::max_value() + isize::max_value() } else { match cap[2].parse() { Ok(x) => x, @@ -115,28 +115,55 @@ mod tests { #[test] fn parse_none_started_range() { let result = Config::parse_choice(":5").unwrap(); - assert_eq!((usize::min_value(), 5), (result.start, result.end)) + assert_eq!((0, 5), (result.start, result.end)) } #[test] fn parse_none_terminated_range() { let result = Config::parse_choice("5:").unwrap(); - assert_eq!((5, usize::max_value()), (result.start, result.end)) + assert_eq!((5, isize::max_value()), (result.start, result.end)) } #[test] - fn parse_full_range() { + fn parse_full_range_pos_pos() { let result = Config::parse_choice("5:7").unwrap(); assert_eq!((5, 7), (result.start, result.end)) } + #[test] + fn parse_full_range_neg_neg() { + let result = Config::parse_choice("-3:-1").unwrap(); + assert_eq!((-3, -1), (result.start, result.end)) + } + + #[test] + fn parse_neg_started_none_ended() { + let result = Config::parse_choice("-3:").unwrap(); + assert_eq!((-3, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_none_started_neg_ended() { + let result = Config::parse_choice(":-1").unwrap(); + assert_eq!((0, -1), (result.start, result.end)) + } + + #[test] + fn parse_full_range_pos_neg() { + let result = Config::parse_choice("5:-3").unwrap(); + assert_eq!((5, -3), (result.start, result.end)) + } + + #[test] + fn parse_full_range_neg_pos() { + let result = Config::parse_choice("-3:5").unwrap(); + assert_eq!((-3, 5), (result.start, result.end)) + } + #[test] fn parse_beginning_to_end_range() { let result = Config::parse_choice(":").unwrap(); - assert_eq!( - (usize::min_value(), usize::max_value()), - (result.start, result.end) - ) + assert_eq!((0, isize::max_value()), (result.start, result.end)) } #[test] diff --git a/src/opt.rs b/src/opt.rs index 9704d4f..77ba9fd 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -6,6 +6,7 @@ use crate::config::Config; #[derive(Debug, StructOpt)] #[structopt(name = "choose", about = "`choose` sections from each line of files")] +#[structopt(setting = structopt::clap::AppSettings::AllowLeadingHyphen)] pub struct Opt { /// Specify field separator other than whitespace #[structopt(short, long)] diff --git a/test/choose_-4:-2.txt b/test/choose_-4:-2.txt new file mode 100644 index 0000000..313a492 --- /dev/null +++ b/test/choose_-4:-2.txt @@ -0,0 +1,6 @@ +sed do eiusmod +ad minim veniam, +ex ea commodo +esse cillum dolore +non proident, sunt +anim id est diff --git a/test/e2e_test.sh b/test/e2e_test.sh index 1f2556a..a415961 100755 --- a/test/e2e_test.sh +++ b/test/e2e_test.sh @@ -13,6 +13,7 @@ diff -w <(cargo run -- 9 3 -i ${test_dir}/lorem.txt 2>/dev/null) <(cat "${test_d diff -w <(cargo run -- 9 -i ${test_dir}/lorem.txt 2>/dev/null) <(cat "${test_dir}/choose_9.txt") diff -w <(cargo run -- 12 -i ${test_dir}/lorem.txt 2>/dev/null) <(cat "${test_dir}/choose_12.txt") diff -w <(cargo run -- 4:2 -i ${test_dir}/lorem.txt 2>/dev/null) <(cat "${test_dir}/choose_4:2.txt") +diff -w <(cargo run -- -4:-2 -i ${test_dir}/lorem.txt 2>/dev/null) <(cat "${test_dir}/choose_-4:-2.txt") # add tests for different delimiters # add tests using piping