From 4201f48be53170ef5e7a55cfe769068cdd6c5d85 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Fri, 2 Oct 2020 14:45:59 -0500 Subject: [PATCH] Left Pad and Right Pad String (#2630) * WIP * left and right pad strings * fixed some tests --- crates/nu-cli/src/cli.rs | 2 + crates/nu-cli/src/commands.rs | 6 +- crates/nu-cli/src/commands/str_/lpad.rs | 179 ++++++++++++++++++++++++ crates/nu-cli/src/commands/str_/mod.rs | 4 + crates/nu-cli/src/commands/str_/rpad.rs | 179 ++++++++++++++++++++++++ 5 files changed, 367 insertions(+), 3 deletions(-) create mode 100644 crates/nu-cli/src/commands/str_/lpad.rs create mode 100644 crates/nu-cli/src/commands/str_/rpad.rs diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index ffdc32d58..8e6df277d 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -147,7 +147,9 @@ pub fn create_default_context(interactive: bool) -> Result, + character: Tagged, + rest: Vec, +} + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str lpad" + } + + fn signature(&self) -> Signature { + Signature::build("str lpad") + .required_named("length", SyntaxShape::Int, "length to pad to", Some('l')) + .required_named( + "character", + SyntaxShape::String, + "character to pad with", + Some('c'), + ) + .rest( + SyntaxShape::ColumnPath, + "optionally check if string contains pattern by column paths", + ) + } + + fn usage(&self) -> &str { + "pad a string with a character a certain length" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Left pad a string with a character a number of places", + example: "echo 'nushell' | str lpad -l 10 -c '*'", + result: Some(vec![ + UntaggedValue::string("***nushell").into_untagged_value() + ]), + }, + Example { + description: "Left pad a string with a character a number of places", + example: "echo '123' | str lpad -l 10 -c '0'", + result: Some(vec![ + UntaggedValue::string("0000000123").into_untagged_value() + ]), + }, + Example { + description: "Use lpad to truncate a string", + example: "echo '123456789' | str lpad -l 3 -c '0'", + result: Some(vec![UntaggedValue::string("123").into_untagged_value()]), + }, + ] + } +} + +async fn operate( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let registry = registry.clone(); + + let ( + Arguments { + length, + character, + rest, + }, + input, + ) = args.process(®istry).await?; + let column_paths: Vec<_> = rest; + + Ok(input + .map(move |v| { + let len = length.item; + let character = character.item; + if column_paths.is_empty() { + ReturnSuccess::value(action(&v, len, character, v.tag())?) + } else { + let mut ret = v; + + for path in &column_paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| action(old, len, character, old.tag())), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +} + +fn action( + input: &Value, + length: usize, + character: char, + tag: impl Into, +) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + if length < s.len() { + Ok(UntaggedValue::string(&s[0..length]).into_value(tag)) + } else { + let mut res = character.to_string().repeat(length - s.len()); + res += s.as_ref(); + Ok(UntaggedValue::string(res).into_value(tag)) + } + } + other => { + let got = format!("got {}", other.type_name()); + Err(ShellError::labeled_error( + "value is not string", + got, + tag.into().span, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::{action, SubCommand}; + use nu_plugin::test_helpers::value::string; + use nu_protocol::UntaggedValue; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn left_pad_with_zeros() { + let word = string("123"); + let pad_char = '0'; + let pad_len = 10; + let expected = UntaggedValue::string("0000000123").into_untagged_value(); + + let actual = action(&word, pad_len, pad_char, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn left_pad_but_truncate() { + let word = string("123456789"); + let pad_char = '0'; + let pad_len = 3; + let expected = UntaggedValue::string("123").into_untagged_value(); + + let actual = action(&word, pad_len, pad_char, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/mod.rs b/crates/nu-cli/src/commands/str_/mod.rs index 67e761558..f205a913d 100644 --- a/crates/nu-cli/src/commands/str_/mod.rs +++ b/crates/nu-cli/src/commands/str_/mod.rs @@ -9,7 +9,9 @@ mod find_replace; pub mod from; mod index_of; mod length; +mod lpad; mod reverse; +mod rpad; mod set; mod starts_with; mod substring; @@ -34,7 +36,9 @@ pub use find_replace::SubCommand as StrFindReplace; pub use from::SubCommand as StrFrom; pub use index_of::SubCommand as StrIndexOf; pub use length::SubCommand as StrLength; +pub use lpad::SubCommand as StrLPad; pub use reverse::SubCommand as StrReverse; +pub use rpad::SubCommand as StrRPad; pub use set::SubCommand as StrSet; pub use starts_with::SubCommand as StrStartsWith; pub use substring::SubCommand as StrSubstring; diff --git a/crates/nu-cli/src/commands/str_/rpad.rs b/crates/nu-cli/src/commands/str_/rpad.rs new file mode 100644 index 000000000..39c318ed4 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/rpad.rs @@ -0,0 +1,179 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::ShellTypeName; +use nu_protocol::{ + ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, +}; +use nu_source::{Tag, Tagged}; +use nu_value_ext::ValueExt; + +#[derive(Deserialize)] +struct Arguments { + length: Tagged, + character: Tagged, + rest: Vec, +} + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str rpad" + } + + fn signature(&self) -> Signature { + Signature::build("str rpad") + .required_named("length", SyntaxShape::Int, "length to pad to", Some('l')) + .required_named( + "character", + SyntaxShape::String, + "character to pad with", + Some('c'), + ) + .rest( + SyntaxShape::ColumnPath, + "optionally check if string contains pattern by column paths", + ) + } + + fn usage(&self) -> &str { + "pad a string with a character a certain length" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Right pad a string with a character a number of places", + example: "echo 'nushell' | str rpad -l 10 -c '*'", + result: Some(vec![ + UntaggedValue::string("nushell***").into_untagged_value() + ]), + }, + Example { + description: "Right pad a string with a character a number of places", + example: "echo '123' | str rpad -l 10 -c '0'", + result: Some(vec![ + UntaggedValue::string("1230000000").into_untagged_value() + ]), + }, + Example { + description: "Use rpad to truncate a string", + example: "echo '123456789' | str rpad -l 3 -c '0'", + result: Some(vec![UntaggedValue::string("123").into_untagged_value()]), + }, + ] + } +} + +async fn operate( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let registry = registry.clone(); + + let ( + Arguments { + length, + character, + rest, + }, + input, + ) = args.process(®istry).await?; + let column_paths: Vec<_> = rest; + + Ok(input + .map(move |v| { + let len = length.item; + let character = character.item; + if column_paths.is_empty() { + ReturnSuccess::value(action(&v, len, character, v.tag())?) + } else { + let mut ret = v; + + for path in &column_paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| action(old, len, character, old.tag())), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +} + +fn action( + input: &Value, + length: usize, + character: char, + tag: impl Into, +) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + if length < s.len() { + Ok(UntaggedValue::string(&s[0..length]).into_value(tag)) + } else { + let mut res = s.to_string(); + res += character.to_string().repeat(length - s.len()).as_str(); + Ok(UntaggedValue::string(res).into_value(tag)) + } + } + other => { + let got = format!("got {}", other.type_name()); + Err(ShellError::labeled_error( + "value is not string", + got, + tag.into().span, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::{action, SubCommand}; + use nu_plugin::test_helpers::value::string; + use nu_protocol::UntaggedValue; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn right_pad_with_zeros() { + let word = string("123"); + let pad_char = '0'; + let pad_len = 10; + let expected = UntaggedValue::string("1230000000").into_untagged_value(); + + let actual = action(&word, pad_len, pad_char, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn right_pad_but_truncate() { + let word = string("123456789"); + let pad_char = '0'; + let pad_len = 3; + let expected = UntaggedValue::string("123").into_untagged_value(); + + let actual = action(&word, pad_len, pad_char, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +}