From 60f9fe1aa4b3f0e8a69b162b75e2c0a921295d69 Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 15:41:39 +1300 Subject: [PATCH 1/2] Port split column and split row --- crates/nu-command/src/default_context.rs | 2 + crates/nu-command/src/strings/split/chars.rs | 78 +++++++------------- crates/nu-command/src/strings/split/mod.rs | 8 +- 3 files changed, 32 insertions(+), 56 deletions(-) diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 9eb6c1c0bc..8b28172a0a 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -43,6 +43,8 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Select)); working_set.add_decl(Box::new(Split)); working_set.add_decl(Box::new(SplitChars)); + working_set.add_decl(Box::new(SplitColumn)); + working_set.add_decl(Box::new(SplitRow)); working_set.add_decl(Box::new(Sys)); working_set.add_decl(Box::new(Table)); working_set.add_decl(Box::new(Touch)); diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs index f77f873692..108190c6aa 100644 --- a/crates/nu-command/src/strings/split/chars.rs +++ b/crates/nu-command/src/strings/split/chars.rs @@ -65,69 +65,21 @@ fn split_chars(call: &Call, input: Value) -> Result Value::List { vals: vals .iter() - .flat_map(move |v| { - if let Ok(s) = v.as_string() { - let v_span = v.span(); - s.chars() - .collect::>() - .into_iter() - .map(move |x| Value::String { - val: x.to_string(), - span: v_span, - }) - .collect() - } else { - vec![Value::Error { - error: ShellError::PipelineMismatch { - expected: Type::String, - expected_span: name, - origin: v.span(), - }, - }] - } - }) + .flat_map(|x| split_chars_helper(x, name)) .collect(), span, }, Value::Stream { stream, span } => Value::Stream { stream: stream - .flat_map(move |v| { - if let Ok(s) = v.as_string() { - let v_span = v.span(); - s.chars() - .collect::>() - .into_iter() - .map(move |x| Value::String { - val: x.to_string(), - span: v_span, - }) - .collect() - } else { - vec![Value::Error { - error: ShellError::PipelineMismatch { - expected: Type::String, - expected_span: name, - origin: v.span(), - }, - }] - } - }) + .flat_map(move |x| split_chars_helper(&x, name)) .into_value_stream(), span, }, v => { let v_span = v.span(); - if let Ok(s) = v.as_string() { + if v.as_string().is_ok() { Value::List { - vals: s - .chars() - .collect::>() - .into_iter() - .map(move |x| Value::String { - val: x.to_string(), - span: v_span, - }) - .collect(), + vals: split_chars_helper(&v, name), span: v_span, } } else { @@ -143,6 +95,28 @@ fn split_chars(call: &Call, input: Value) -> Result Vec { + if let Ok(s) = v.as_string() { + let v_span = v.span(); + s.chars() + .collect::>() + .into_iter() + .map(move |x| Value::String { + val: x.to_string(), + span: v_span, + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + }] + } +} + // #[cfg(test)] // mod tests { // use super::ShellError; diff --git a/crates/nu-command/src/strings/split/mod.rs b/crates/nu-command/src/strings/split/mod.rs index 3a65f0e124..c6e6da0171 100644 --- a/crates/nu-command/src/strings/split/mod.rs +++ b/crates/nu-command/src/strings/split/mod.rs @@ -1,9 +1,9 @@ pub mod chars; -// pub mod column; +pub mod column; pub mod command; -// pub mod row; +pub mod row; pub use chars::SubCommand as SplitChars; -// pub use column::SubCommand as SplitColumn; +pub use column::SubCommand as SplitColumn; pub use command::SplitCommand as Split; -// pub use row::SubCommand as SplitRow; +pub use row::SubCommand as SplitRow; From 5c29a83a7ace824789a79137cf5fbdad116ecf6e Mon Sep 17 00:00:00 2001 From: JT Date: Sat, 9 Oct 2021 15:45:25 +1300 Subject: [PATCH 2/2] Add tests --- crates/nu-command/src/strings/split/column.rs | 164 ++++++++++++++++++ crates/nu-command/src/strings/split/row.rs | 116 +++++++++++++ src/tests.rs | 13 ++ 3 files changed, 293 insertions(+) create mode 100644 crates/nu-command/src/strings/split/column.rs create mode 100644 crates/nu-command/src/strings/split/row.rs diff --git a/crates/nu-command/src/strings/split/column.rs b/crates/nu-command/src/strings/split/column.rs new file mode 100644 index 0000000000..fb3836ee3e --- /dev/null +++ b/crates/nu-command/src/strings/split/column.rs @@ -0,0 +1,164 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EvaluationContext}, + IntoValueStream, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, +}; + +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split column" + } + + fn signature(&self) -> Signature { + Signature::build("split column") + .required( + "separator", + SyntaxShape::String, + "the character that denotes what separates columns", + ) + .switch("collapse-empty", "remove empty columns", Some('c')) + .rest( + "rest", + SyntaxShape::String, + "column names to give the new columns", + ) + } + + fn usage(&self) -> &str { + "splits contents across multiple columns via the separator." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + split_column(context, call, input) + } +} + +fn split_column( + context: &EvaluationContext, + call: &Call, + input: Value, +) -> Result { + let name_span = call.head; + let separator: Spanned = call.req(context, 0)?; + let rest: Vec> = call.rest(context, 1)?; + let collapse_empty = call.has_flag("collapse-empty"); + + Ok(match input { + Value::List { vals, span } => Value::List { + vals: vals + .iter() + .map(move |x| split_column_helper(x, &separator, &rest, collapse_empty, name_span)) + .collect(), + span, + }, + Value::Stream { stream, span } => Value::Stream { + stream: stream + .map(move |x| split_column_helper(&x, &separator, &rest, collapse_empty, name_span)) + .into_value_stream(), + span, + }, + v => { + if v.as_string().is_ok() { + Value::List { + vals: vec![split_column_helper( + &v, + &separator, + &rest, + collapse_empty, + name_span, + )], + span: call.head, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: call.head, + origin: v.span(), + }, + } + } + } + }) +} + +fn split_column_helper( + v: &Value, + separator: &Spanned, + rest: &[Spanned], + collapse_empty: bool, + head: Span, +) -> Value { + if let Ok(s) = v.as_string() { + let splitter = separator.item.replace("\\n", "\n"); + + let split_result: Vec<_> = if collapse_empty { + s.split(&splitter).filter(|s| !s.is_empty()).collect() + } else { + s.split(&splitter).collect() + }; + + let positional: Vec<_> = rest.iter().map(|f| f.item.clone()).collect(); + + // If they didn't provide column names, make up our own + + let mut cols = vec![]; + let mut vals = vec![]; + if positional.is_empty() { + let mut gen_columns = vec![]; + for i in 0..split_result.len() { + gen_columns.push(format!("Column{}", i + 1)); + } + + for (&k, v) in split_result.iter().zip(&gen_columns) { + cols.push(v.to_string()); + vals.push(Value::String { + val: k.into(), + span: head, + }); + } + } else { + for (&k, v) in split_result.iter().zip(&positional) { + cols.push(v.into()); + vals.push(Value::String { + val: k.into(), + span: head, + }) + } + } + Value::Record { + cols, + vals, + span: head, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: head, + origin: v.span(), + }, + } + } +} + +// #[cfg(test)] +// mod tests { +// use super::ShellError; +// use super::SubCommand; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(SubCommand {}) +// } +// } diff --git a/crates/nu-command/src/strings/split/row.rs b/crates/nu-command/src/strings/split/row.rs new file mode 100644 index 0000000000..a4602e2ca4 --- /dev/null +++ b/crates/nu-command/src/strings/split/row.rs @@ -0,0 +1,116 @@ +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EvaluationContext}, + IntoValueStream, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, +}; + +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split row" + } + + fn signature(&self) -> Signature { + Signature::build("split row").required( + "separator", + SyntaxShape::String, + "the character that denotes what separates rows", + ) + } + + fn usage(&self) -> &str { + "splits contents over multiple rows via the separator." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + split_row(context, call, input) + } +} + +fn split_row( + context: &EvaluationContext, + call: &Call, + input: Value, +) -> Result { + let name_span = call.head; + let separator: Spanned = call.req(context, 0)?; + + Ok(match input { + Value::List { vals, span } => Value::List { + vals: vals + .iter() + .flat_map(move |x| split_row_helper(x, &separator, name_span)) + .collect(), + span, + }, + Value::Stream { stream, span } => Value::Stream { + stream: stream + .flat_map(move |x| split_row_helper(&x, &separator, name_span)) + .into_value_stream(), + span, + }, + v => { + let v_span = v.span(); + if v.as_string().is_ok() { + Value::List { + vals: split_row_helper(&v, &separator, name_span), + span: v_span, + } + } else { + Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: call.head, + origin: v.span(), + }, + } + } + } + }) +} + +fn split_row_helper(v: &Value, separator: &Spanned, name: Span) -> Vec { + if let Ok(s) = v.as_string() { + let splitter = separator.item.replace("\\n", "\n"); + s.split(&splitter) + .filter_map(|s| { + if s.trim() != "" { + Some(Value::String { + val: s.into(), + span: v.span(), + }) + } else { + None + } + }) + .collect() + } else { + vec![Value::Error { + error: ShellError::PipelineMismatch { + expected: Type::String, + expected_span: name, + origin: v.span(), + }, + }] + } +} + +// #[cfg(test)] +// mod tests { +// use super::ShellError; +// use super::SubCommand; + +// #[test] +// fn examples_work_as_expected() -> Result<(), ShellError> { +// use crate::examples::test as test_examples; + +// test_examples(SubCommand {}) +// } +// } diff --git a/src/tests.rs b/src/tests.rs index 0efd0b513a..b3921acbf4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -540,3 +540,16 @@ fn string_cell_path() -> TestResult { "c", ) } + +#[test] +fn split_row() -> TestResult { + run_test(r#""hello world" | split row " " | get 1"#, "world") +} + +#[test] +fn split_column() -> TestResult { + run_test( + r#""hello world" | split column " " | get "Column1".0"#, + "hello", + ) +}