Rows and values can be checked for emptiness. Allows to set a value if desired. (#1665)

This commit is contained in:
Andrés N. Robalino 2020-04-26 12:30:52 -05:00 committed by GitHub
parent a62745eefb
commit 80025ea684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 437 additions and 98 deletions

View File

@ -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),

View File

@ -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;

View File

@ -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<Tagged<ColumnPath>>, Value),
RowWithField(Tagged<ColumnPath>),
RowWithFieldAndFallback(Box<Tagged<ColumnPath>>, 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<OutputStream, ShellError> {
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())
}
}
}
}

View File

@ -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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>(),
&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::<Vec<_>>(),
)
.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::<Vec<_>>(),
&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::<Vec<_>>(),
)
.expect("Couldn't join paths.")
.into_string()

View File

@ -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;

View File

@ -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");
})
}

View File

@ -16,6 +16,7 @@ mod group_by;
mod headers;
mod histogram;
mod insert;
mod is_empty;
mod last;
mod lines;
mod ls;

View File

@ -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<Value> for String {

View File

@ -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<Self::Item> {
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<Self::Item> {
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,
}
}

View File

@ -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 {

View File

@ -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<Value>;
@ -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<Self::Item> {
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<Self::Item> {
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,
}
}