From 80025ea684db049601cfa1917202500ed4d7fbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Sun, 26 Apr 2020 12:30:52 -0500 Subject: [PATCH] Rows and values can be checked for emptiness. Allows to set a value if desired. (#1665) --- crates/nu-cli/src/cli.rs | 1 + crates/nu-cli/src/commands.rs | 2 + crates/nu-cli/src/commands/is_empty.rs | 203 ++++++++++++++++++++ crates/nu-cli/src/env/environment_syncer.rs | 77 ++++---- crates/nu-cli/src/utils.rs | 2 +- crates/nu-cli/tests/commands/is_empty.rs | 96 +++++++++ crates/nu-cli/tests/commands/mod.rs | 1 + crates/nu-protocol/src/value.rs | 35 ++++ crates/nu-protocol/src/value/iter.rs | 50 +++++ crates/nu-protocol/src/value/primitive.rs | 9 + crates/nu-value-ext/src/lib.rs | 59 ------ 11 files changed, 437 insertions(+), 98 deletions(-) create mode 100644 crates/nu-cli/src/commands/is_empty.rs create mode 100644 crates/nu-cli/tests/commands/is_empty.rs create mode 100644 crates/nu-protocol/src/value/iter.rs diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 3249f8bc49..c9fae069c9 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -307,6 +307,7 @@ pub fn create_default_context( whole_stream_command(Rename), whole_stream_command(Uniq), per_item_command(Each), + per_item_command(IsEmpty), // Table manipulation whole_stream_command(Shuffle), whole_stream_command(Wrap), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 60d3098e03..1e54e58e2a 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -54,6 +54,7 @@ pub(crate) mod help; pub(crate) mod histogram; pub(crate) mod history; pub(crate) mod insert; +pub(crate) mod is_empty; pub(crate) mod last; pub(crate) mod lines; pub(crate) mod ls; @@ -135,6 +136,7 @@ pub(crate) use du::Du; pub(crate) use each::Each; pub(crate) use echo::Echo; pub(crate) use edit::Edit; +pub(crate) use is_empty::IsEmpty; pub(crate) mod kill; pub(crate) use kill::Kill; pub(crate) mod clear; diff --git a/crates/nu-cli/src/commands/is_empty.rs b/crates/nu-cli/src/commands/is_empty.rs new file mode 100644 index 0000000000..db7f2d3080 --- /dev/null +++ b/crates/nu-cli/src/commands/is_empty.rs @@ -0,0 +1,203 @@ +use crate::commands::PerItemCommand; +use crate::context::CommandRegistry; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ + CallInfo, ColumnPath, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, +}; +use nu_source::Tagged; +use nu_value_ext::ValueExt; + +enum IsEmptyFor { + Value, + RowWithFieldsAndFallback(Vec>, Value), + RowWithField(Tagged), + RowWithFieldAndFallback(Box>, Value), +} + +pub struct IsEmpty; + +impl PerItemCommand for IsEmpty { + fn name(&self) -> &str { + "empty?" + } + + fn signature(&self) -> Signature { + Signature::build("empty?").rest( + SyntaxShape::Any, + "the names of the columns to check emptiness followed by the replacement value.", + ) + } + + fn usage(&self) -> &str { + "Checks emptiness. The last value is the replacement value for any empty column(s) given to check against the table." + } + + fn run( + &self, + call_info: &CallInfo, + _registry: &CommandRegistry, + _raw_args: &RawCommandArgs, + value: Value, + ) -> Result { + let value_tag = value.tag(); + + let action = if call_info.args.len() <= 2 { + let field = call_info.args.expect_nth(0); + let replacement_if_true = call_info.args.expect_nth(1); + + match (field, replacement_if_true) { + (Ok(field), Ok(replacement_if_true)) => IsEmptyFor::RowWithFieldAndFallback( + Box::new(field.as_column_path()?), + replacement_if_true.clone(), + ), + (Ok(field), Err(_)) => IsEmptyFor::RowWithField(field.as_column_path()?), + (_, _) => IsEmptyFor::Value, + } + } else { + let no_args = vec![]; + let mut arguments = call_info + .args + .positional + .as_ref() + .unwrap_or_else(|| &no_args) + .iter() + .rev(); + let replacement_if_true = match arguments.next() { + Some(arg) => arg.clone(), + None => UntaggedValue::boolean(value.is_empty()).into_value(&value_tag), + }; + + IsEmptyFor::RowWithFieldsAndFallback( + arguments + .map(|a| a.as_column_path()) + .filter_map(Result::ok) + .collect(), + replacement_if_true, + ) + }; + + match action { + IsEmptyFor::Value => Ok(futures::stream::iter(vec![Ok(ReturnSuccess::Value( + UntaggedValue::boolean(value.is_empty()).into_value(value_tag), + ))]) + .to_output_stream()), + IsEmptyFor::RowWithFieldsAndFallback(fields, default) => { + let mut out = value; + + for field in fields.iter() { + let val = + out.get_data_by_column_path(&field, Box::new(move |(_, _, err)| err))?; + + let emptiness_value = match out { + obj + @ + Value { + value: UntaggedValue::Row(_), + .. + } => { + if val.is_empty() { + match obj.replace_data_at_column_path(&field, default.clone()) { + Some(v) => Ok(v), + None => Err(ShellError::labeled_error( + "empty? could not find place to check emptiness", + "column name", + &field.tag, + )), + } + } else { + Ok(obj) + } + } + _ => Err(ShellError::labeled_error( + "Unrecognized type in stream", + "original value", + &value_tag, + )), + }; + + out = emptiness_value?; + } + + Ok(futures::stream::iter(vec![Ok(ReturnSuccess::Value(out))]).to_output_stream()) + } + IsEmptyFor::RowWithField(field) => { + let val = + value.get_data_by_column_path(&field, Box::new(move |(_, _, err)| err))?; + + let stream = match &value { + obj + @ + Value { + value: UntaggedValue::Row(_), + .. + } => { + if val.is_empty() { + match obj.replace_data_at_column_path( + &field, + UntaggedValue::boolean(true).into_value(&value_tag), + ) { + Some(v) => futures::stream::iter(vec![Ok(ReturnSuccess::Value(v))]), + None => { + return Err(ShellError::labeled_error( + "empty? could not find place to check emptiness", + "column name", + &field.tag, + )) + } + } + } else { + futures::stream::iter(vec![Ok(ReturnSuccess::Value(value))]) + } + } + _ => { + return Err(ShellError::labeled_error( + "Unrecognized type in stream", + "original value", + &value_tag, + )) + } + }; + + Ok(stream.to_output_stream()) + } + IsEmptyFor::RowWithFieldAndFallback(field, default) => { + let val = + value.get_data_by_column_path(&field, Box::new(move |(_, _, err)| err))?; + + let stream = match &value { + obj + @ + Value { + value: UntaggedValue::Row(_), + .. + } => { + if val.is_empty() { + match obj.replace_data_at_column_path(&field, default) { + Some(v) => futures::stream::iter(vec![Ok(ReturnSuccess::Value(v))]), + None => { + return Err(ShellError::labeled_error( + "empty? could not find place to check emptiness", + "column name", + &field.tag, + )) + } + } + } else { + futures::stream::iter(vec![Ok(ReturnSuccess::Value(value))]) + } + } + _ => { + return Err(ShellError::labeled_error( + "Unrecognized type in stream", + "original value", + &value_tag, + )) + } + }; + + Ok(stream.to_output_stream()) + } + } + } +} diff --git a/crates/nu-cli/src/env/environment_syncer.rs b/crates/nu-cli/src/env/environment_syncer.rs index 9c599e34ed..852ccfa75c 100644 --- a/crates/nu-cli/src/env/environment_syncer.rs +++ b/crates/nu-cli/src/env/environment_syncer.rs @@ -58,7 +58,7 @@ impl EnvironmentSyncer { } if let Some(variables) = environment.env() { - for var in nu_value_ext::row_entries(&variables) { + for var in variables.row_entries() { if let Ok(string) = var.1.as_string() { ctx.with_host(|host| { host.env_set( @@ -88,7 +88,8 @@ impl EnvironmentSyncer { if let Some(new_paths) = environment.path() { let prepared = std::env::join_paths( - nu_value_ext::table_entries(&new_paths) + new_paths + .table_entries() .map(|p| p.as_string()) .filter_map(Result::ok), ); @@ -212,16 +213,17 @@ mod tests { // including the newer one accounted for. let environment = actual.env.lock(); - let vars = nu_value_ext::row_entries( - &environment.env().expect("No variables in the environment."), - ) - .map(|(name, value)| { - ( - name.to_string(), - value.as_string().expect("Couldn't convert to string"), - ) - }) - .collect::>(); + let vars = environment + .env() + .expect("No variables in the environment.") + .row_entries() + .map(|(name, value)| { + ( + name.to_string(), + value.as_string().expect("Couldn't convert to string"), + ) + }) + .collect::>(); assert_eq!(vars, expected); }); @@ -281,16 +283,17 @@ mod tests { let environment = actual.env.lock(); - let vars = nu_value_ext::row_entries( - &environment.env().expect("No variables in the environment."), - ) - .map(|(name, value)| { - ( - name.to_string(), - value.as_string().expect("Couldn't convert to string"), - ) - }) - .collect::>(); + let vars = environment + .env() + .expect("No variables in the environment.") + .row_entries() + .map(|(name, value)| { + ( + name.to_string(), + value.as_string().expect("Couldn't convert to string"), + ) + }) + .collect::>(); assert_eq!(vars, expected); }); @@ -367,14 +370,13 @@ mod tests { let environment = actual.env.lock(); let paths = std::env::join_paths( - &nu_value_ext::table_entries( - &environment - .path() - .expect("No path variable in the environment."), - ) - .map(|value| value.as_string().expect("Couldn't convert to string")) - .map(PathBuf::from) - .collect::>(), + &environment + .path() + .expect("No path variable in the environment.") + .table_entries() + .map(|value| value.as_string().expect("Couldn't convert to string")) + .map(PathBuf::from) + .collect::>(), ) .expect("Couldn't join paths.") .into_string() @@ -442,14 +444,13 @@ mod tests { let environment = actual.env.lock(); let paths = std::env::join_paths( - &nu_value_ext::table_entries( - &environment - .path() - .expect("No path variable in the environment."), - ) - .map(|value| value.as_string().expect("Couldn't convert to string")) - .map(PathBuf::from) - .collect::>(), + &environment + .path() + .expect("No path variable in the environment.") + .table_entries() + .map(|value| value.as_string().expect("Couldn't convert to string")) + .map(PathBuf::from) + .collect::>(), ) .expect("Couldn't join paths.") .into_string() diff --git a/crates/nu-cli/src/utils.rs b/crates/nu-cli/src/utils.rs index b03124b140..d3c7e3d724 100644 --- a/crates/nu-cli/src/utils.rs +++ b/crates/nu-cli/src/utils.rs @@ -69,7 +69,7 @@ impl ValueStructure { } fn build(&mut self, src: &Value, lvl: usize) -> Result<(), ShellError> { - for entry in nu_value_ext::row_entries(src) { + for entry in src.row_entries() { let value = entry.1; let path = entry.0; diff --git a/crates/nu-cli/tests/commands/is_empty.rs b/crates/nu-cli/tests/commands/is_empty.rs new file mode 100644 index 0000000000..1d3181743c --- /dev/null +++ b/crates/nu-cli/tests/commands/is_empty.rs @@ -0,0 +1,96 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn adds_value_provided_if_column_is_empty() { + Playground::setup("is_empty_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "likes.csv", + r#" + first_name,last_name,rusty_at,likes + Andrés,Robalino,10/11/2013,1 + Jonathan,Turner,10/12/2013,1 + Jason,Gedge,10/11/2013,1 + Yehuda,Katz,10/11/2013, + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open likes.csv + | empty? likes 1 + | get likes + | sum + | echo $it + "# + )); + + assert_eq!(actual, "4"); + }) +} + +#[test] +fn adds_value_provided_for_columns_that_are_empty() { + Playground::setup("is_empty_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "checks.json", + r#" + [ + {"boost": 1, "check": []}, + {"boost": 1, "check": ""}, + {"boost": 1, "check": {}}, + {"boost": null, "check": ["" {} [] ""]} + ] + + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open checks.json + | empty? boost check 1 + | get boost check + | sum + | echo $it + "# + )); + + assert_eq!(actual, "8"); + }) +} + +#[test] +fn value_emptiness_check() { + Playground::setup("is_empty_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "checks.json", + r#" + { + "are_empty": [ + {"check": []}, + {"check": ""}, + {"check": {}}, + {"check": ["" {} [] ""]} + ] + } + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open checks.json + | get are_empty.check + | empty? + | where $it + | count + | echo $it + "# + )); + + assert_eq!(actual, "4"); + }) +} diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index aa1ac93ad9..0303275c1f 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -16,6 +16,7 @@ mod group_by; mod headers; mod histogram; mod insert; +mod is_empty; mod last; mod lines; mod ls; diff --git a/crates/nu-protocol/src/value.rs b/crates/nu-protocol/src/value.rs index 202637a0d6..fc2c5bf077 100644 --- a/crates/nu-protocol/src/value.rs +++ b/crates/nu-protocol/src/value.rs @@ -3,6 +3,7 @@ mod convert; mod debug; pub mod dict; pub mod evaluate; +pub mod iter; pub mod primitive; pub mod range; mod serde_bigdecimal; @@ -11,6 +12,7 @@ mod serde_bigint; use crate::hir; use crate::type_name::{ShellTypeName, SpannedTypeName}; use crate::value::dict::Dictionary; +use crate::value::iter::{RowValueIter, TableValueIter}; use crate::value::primitive::Primitive; use crate::value::range::{Range, RangeInclusion}; use crate::{ColumnPath, PathMember}; @@ -313,6 +315,39 @@ impl Value { _ => Err(ShellError::type_error("boolean", self.spanned_type_name())), } } + + /// Returns an iterator of the values rows + pub fn table_entries(&self) -> TableValueIter<'_> { + crate::value::iter::table_entries(&self) + } + + /// Returns an iterator of the value's cells + pub fn row_entries(&self) -> RowValueIter<'_> { + crate::value::iter::row_entries(&self) + } + + /// Returns true if the value is empty + pub fn is_empty(&self) -> bool { + match &self { + Value { + value: UntaggedValue::Primitive(p), + .. + } => p.is_empty(), + t + @ + Value { + value: UntaggedValue::Table(_), + .. + } => t.table_entries().all(|row| row.is_empty()), + r + @ + Value { + value: UntaggedValue::Row(_), + .. + } => r.row_entries().all(|(_, value)| value.is_empty()), + _ => false, + } + } } impl Into for String { diff --git a/crates/nu-protocol/src/value/iter.rs b/crates/nu-protocol/src/value/iter.rs new file mode 100644 index 0000000000..b1768f7016 --- /dev/null +++ b/crates/nu-protocol/src/value/iter.rs @@ -0,0 +1,50 @@ +use crate::value::{UntaggedValue, Value}; + +pub enum RowValueIter<'a> { + Empty, + Entries(indexmap::map::Iter<'a, String, Value>), +} + +pub enum TableValueIter<'a> { + Empty, + Entries(std::slice::Iter<'a, Value>), +} + +impl<'a> Iterator for RowValueIter<'a> { + type Item = (&'a String, &'a Value); + + fn next(&mut self) -> Option { + match self { + RowValueIter::Empty => None, + RowValueIter::Entries(iter) => iter.next(), + } + } +} + +impl<'a> Iterator for TableValueIter<'a> { + type Item = &'a Value; + + fn next(&mut self) -> Option { + match self { + TableValueIter::Empty => None, + TableValueIter::Entries(iter) => iter.next(), + } + } +} + +pub fn table_entries(value: &Value) -> TableValueIter<'_> { + match &value.value { + UntaggedValue::Table(t) => TableValueIter::Entries(t.iter()), + _ => TableValueIter::Empty, + } +} + +pub fn row_entries(value: &Value) -> RowValueIter<'_> { + match &value.value { + UntaggedValue::Row(o) => { + let iter = o.entries.iter(); + RowValueIter::Entries(iter) + } + _ => RowValueIter::Empty, + } +} diff --git a/crates/nu-protocol/src/value/primitive.rs b/crates/nu-protocol/src/value/primitive.rs index 7ee5155555..d517901ccf 100644 --- a/crates/nu-protocol/src/value/primitive.rs +++ b/crates/nu-protocol/src/value/primitive.rs @@ -84,6 +84,15 @@ impl Primitive { )), } } + + /// Returns true if the value is empty + pub fn is_empty(&self) -> bool { + match self { + Primitive::Nothing => true, + Primitive::String(s) => s.is_empty(), + _ => false, + } + } } impl num_traits::Zero for Primitive { diff --git a/crates/nu-value-ext/src/lib.rs b/crates/nu-value-ext/src/lib.rs index b401153ad1..6f76d82121 100644 --- a/crates/nu-value-ext/src/lib.rs +++ b/crates/nu-value-ext/src/lib.rs @@ -8,8 +8,6 @@ use nu_source::{HasSpan, PrettyDebug, Spanned, SpannedItem, Tag, Tagged, TaggedI use num_traits::cast::ToPrimitive; pub trait ValueExt { - fn row_entries(&self) -> RowValueIter<'_>; - fn table_entries(&self) -> TableValueIter<'_>; fn into_parts(self) -> (UntaggedValue, Tag); fn get_data(&self, desc: &str) -> MaybeOwned<'_, Value>; fn get_data_by_key(&self, name: Spanned<&str>) -> Option; @@ -41,14 +39,6 @@ pub trait ValueExt { } impl ValueExt for Value { - fn row_entries(&self) -> RowValueIter<'_> { - row_entries(self) - } - - fn table_entries(&self) -> TableValueIter<'_> { - table_entries(self) - } - fn into_parts(self) -> (UntaggedValue, Tag) { (self.value, self.tag) } @@ -534,52 +524,3 @@ pub(crate) fn get_mut_data_by_member<'value>( _ => None, } } - -pub enum RowValueIter<'a> { - Empty, - Entries(indexmap::map::Iter<'a, String, Value>), -} - -pub enum TableValueIter<'a> { - Empty, - Entries(std::slice::Iter<'a, Value>), -} - -impl<'a> Iterator for RowValueIter<'a> { - type Item = (&'a String, &'a Value); - - fn next(&mut self) -> Option { - match self { - RowValueIter::Empty => None, - RowValueIter::Entries(iter) => iter.next(), - } - } -} - -impl<'a> Iterator for TableValueIter<'a> { - type Item = &'a Value; - - fn next(&mut self) -> Option { - match self { - TableValueIter::Empty => None, - TableValueIter::Entries(iter) => iter.next(), - } - } -} - -pub fn table_entries(value: &Value) -> TableValueIter<'_> { - match &value.value { - UntaggedValue::Table(t) => TableValueIter::Entries(t.iter()), - _ => TableValueIter::Empty, - } -} - -pub fn row_entries(value: &Value) -> RowValueIter<'_> { - match &value.value { - UntaggedValue::Row(o) => { - let iter = o.entries.iter(); - RowValueIter::Entries(iter) - } - _ => RowValueIter::Empty, - } -}