From 0fdb9ac5e277b6f50d1c7129ce469a8c9fe6fca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Wed, 8 Jul 2020 04:45:45 -0500 Subject: [PATCH] str substring additions. (#2140) --- crates/nu-cli/src/commands/str_/substring.rs | 296 +++++++++++++++---- 1 file changed, 238 insertions(+), 58 deletions(-) diff --git a/crates/nu-cli/src/commands/str_/substring.rs b/crates/nu-cli/src/commands/str_/substring.rs index 5e4c649af..1f4ed5a1a 100644 --- a/crates/nu-cli/src/commands/str_/substring.rs +++ b/crates/nu-cli/src/commands/str_/substring.rs @@ -5,14 +5,16 @@ use nu_protocol::ShellTypeName; use nu_protocol::{ ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, }; -use nu_source::{Tag, Tagged}; -use nu_value_ext::ValueExt; +use nu_source::Tag; +use nu_value_ext::{as_string, ValueExt}; use std::cmp; +use std::cmp::Ordering; +use std::convert::TryInto; #[derive(Deserialize)] struct Arguments { - range: Tagged, + range: Value, rest: Vec, } @@ -28,8 +30,8 @@ impl WholeStreamCommand for SubCommand { Signature::build("str substring") .required( "range", - SyntaxShape::String, - "the indexes to substring \"start, end\"", + SyntaxShape::Any, + "the indexes to substring [start end]", ) .rest( SyntaxShape::ColumnPath, @@ -53,9 +55,19 @@ impl WholeStreamCommand for SubCommand { vec![ Example { description: "Get a substring from the text", + example: "echo 'good nushell' | str substring [5 12]", + result: Some(vec![Value::from("nushell")]), + }, + Example { + description: "Alternatively, you can use the form", example: "echo 'good nushell' | str substring '5,12'", result: Some(vec![Value::from("nushell")]), }, + Example { + description: "Get the last characters from the string", + example: "echo 'good nushell' | str substring ',-5'", + result: Some(vec![Value::from("shell")]), + }, Example { description: "Get the remaining characters from a starting index", example: "echo 'good nushell' | str substring '5,'", @@ -71,7 +83,15 @@ impl WholeStreamCommand for SubCommand { } #[derive(Clone)] -struct Substring(usize, usize); +struct Substring(isize, isize); + +impl From<(isize, isize)> for Substring { + fn from(input: (isize, isize)) -> Substring { + Substring(input.0, input.1) + } +} + +struct SubstringText(String, String); async fn operate( args: CommandArgs, @@ -82,41 +102,8 @@ async fn operate( let (Arguments { range, rest }, input) = args.process(®istry).await?; - let v: Vec<&str> = range.item.split(',').collect(); - - let start = match v[0] { - "" => 0, - _ => v[0].trim().parse().map_err(|_| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - ) - })?, - }; - - let end = match v[1] { - "" => usize::max_value(), - _ => v[1].trim().parse().map_err(|_| { - ShellError::labeled_error( - "could not perform substring", - "could not perform substring", - name.span, - ) - })?, - }; - - if start > end { - return Err(ShellError::labeled_error( - "End must be greater than or equal to Start", - "End must be greater than or equal to Start", - name.span, - )); - } - - let options = Substring(start, end); - let column_paths: Vec<_> = rest; + let options = process_arguments(range, name)?.into(); Ok(input .map(move |v| { @@ -153,35 +140,187 @@ async fn operate( } fn action(input: &Value, options: &Substring, tag: impl Into) -> Result { + let tag = tag.into(); + match &input.value { UntaggedValue::Primitive(Primitive::Line(s)) | UntaggedValue::Primitive(Primitive::String(s)) => { - let start = options.0; - let end: usize = cmp::min(options.1, s.len()); + let len: isize = s.len().try_into().map_err(|_| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + tag.span, + ) + })?; - let out = { - if start > s.len() - 1 { - UntaggedValue::string("") - } else { - UntaggedValue::string( - s.chars().skip(start).take(end - start).collect::(), - ) + let start: isize = options.0.try_into().map_err(|_| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + tag.span, + ) + })?; + + let end = options.1; + + if start < len && end >= 0 { + match start.cmp(&end) { + Ordering::Equal => Ok(UntaggedValue::string("").into_value(tag)), + Ordering::Greater => Err(ShellError::labeled_error( + "End must be greater than or equal to Start", + "End must be greater than or equal to Start", + tag.span, + )), + Ordering::Less => { + let end: isize = cmp::min(options.1, len); + + Ok(UntaggedValue::string( + s.chars() + .skip(start as usize) + .take((end - start) as usize) + .collect::(), + ) + .into_value(tag)) + } } - }; + } else if start >= 0 && end <= 0 { + let end = options.1.abs(); + let reversed = s + .chars() + .skip(start as usize) + .take((len - start) as usize) + .collect::(); - Ok(out.into_value(tag)) + let reversed = if start == 0 { + reversed + } else { + s.chars().take(start as usize).collect::() + }; + + let reversed = reversed + .chars() + .rev() + .take(end as usize) + .collect::(); + + Ok( + UntaggedValue::string(reversed.chars().rev().collect::()) + .into_value(tag), + ) + } else { + Ok(UntaggedValue::string("").into_value(tag)) + } } other => { let got = format!("got {}", other.type_name()); Err(ShellError::labeled_error( "value is not string", got, - tag.into().span, + tag.span, )) } } } +fn process_arguments(range: Value, name: impl Into) -> Result<(isize, isize), ShellError> { + let name = name.into(); + + let search = match &range.value { + UntaggedValue::Table(indexes) => { + if indexes.len() > 2 { + Err(ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + )) + } else { + let idx: Vec = indexes + .iter() + .map(|v| as_string(v).unwrap_or_else(|_| String::from(""))) + .collect(); + + let start = idx + .get(0) + .ok_or_else(|| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + ) + })? + .to_string(); + let end = idx + .get(1) + .ok_or_else(|| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + ) + })? + .to_string(); + + Ok(SubstringText(start, end)) + } + } + UntaggedValue::Primitive(Primitive::String(indexes)) => { + let idx: Vec<&str> = indexes.split(',').collect(); + + let start = idx + .get(0) + .ok_or_else(|| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + ) + })? + .to_string(); + let end = idx + .get(1) + .ok_or_else(|| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + ) + })? + .to_string(); + + Ok(SubstringText(start, end)) + } + _ => Err(ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + )), + }?; + + let start = match &search { + SubstringText(start, _) if start == "" || start == "_" => 0, + SubstringText(start, _) => start.trim().parse().map_err(|_| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + ) + })?, + }; + + let end = match &search { + SubstringText(_, end) if end == "" || end == "_" => isize::max_value(), + SubstringText(_, end) => end.trim().parse().map_err(|_| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + ) + })?, + }; + + Ok((start, end)) +} + #[cfg(test)] mod tests { use super::{action, SubCommand, Substring}; @@ -195,14 +334,55 @@ mod tests { test_examples(SubCommand {}) } + struct Expectation<'a> { + options: (isize, isize), + expected: &'a str, + } + + impl Expectation<'_> { + fn options(&self) -> Substring { + Substring(self.options.0, self.options.1) + } + } + + fn expectation(word: &str, indexes: (isize, isize)) -> Expectation { + Expectation { + options: indexes, + expected: word, + } + } + #[test] - fn given_start_and_end_indexes() { - let word = string("andresS"); - let expected = string("andres"); + fn substrings_indexes() { + let word = string("andres"); - let substring_options = Substring(0, 6); + let cases = vec![ + expectation("a", (0, 1)), + expectation("an", (0, 2)), + expectation("and", (0, 3)), + expectation("andr", (0, 4)), + expectation("andre", (0, 5)), + expectation("andres", (0, 6)), + expectation("andres", (0, -6)), + expectation("ndres", (0, -5)), + expectation("dres", (0, -4)), + expectation("res", (0, -3)), + expectation("es", (0, -2)), + expectation("s", (0, -1)), + expectation("", (6, 0)), + expectation("s", (6, -1)), + expectation("es", (6, -2)), + expectation("res", (6, -3)), + expectation("dres", (6, -4)), + expectation("ndres", (6, -5)), + expectation("andres", (6, -6)), + ]; - let actual = action(&word, &substring_options, Tag::unknown()).unwrap(); - assert_eq!(actual, expected); + for expectation in cases.iter() { + let expected = expectation.expected; + let actual = action(&word, &expectation.options(), Tag::unknown()).unwrap(); + + assert_eq!(actual, string(expected)); + } } }