-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
We introduce it here and allow it to work with regular lists (tables with no columns) as well as symmetric tables. Say we have two lists and wish to zip them, like so: ``` [0 2 4 6 8] | zip { [1 3 5 7 9] } | flatten ───┬─── 0 │ 0 1 │ 1 2 │ 2 3 │ 3 4 │ 4 5 │ 5 6 │ 6 7 │ 7 8 │ 8 9 │ 9 ───┴─── ``` In the case for two tables instead: ``` [[symbol]; ['('] ['['] ['{']] | zip { [[symbol]; [')'] [']'] ['}']] } | each { get symbol | $'($in.0)nushell($in.1)' } ───┬─────────── 0 │ (nushell) 1 │ [nushell] 2 │ {nushell} ───┴─────────── ```
- Loading branch information
Showing
7 changed files
with
264 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
use crate::prelude::*; | ||
use nu_engine::run_block; | ||
use nu_engine::WholeStreamCommand; | ||
use nu_errors::ShellError; | ||
use nu_protocol::did_you_mean; | ||
use nu_protocol::TaggedDictBuilder; | ||
use nu_protocol::{ | ||
hir::CapturedBlock, hir::ExternalRedirection, ColumnPath, PathMember, Signature, SyntaxShape, | ||
UnspannedPathMember, UntaggedValue, Value, | ||
}; | ||
use nu_value_ext::get_data_by_column_path; | ||
|
||
use nu_source::HasFallibleSpan; | ||
pub struct Command; | ||
|
||
impl WholeStreamCommand for Command { | ||
fn name(&self) -> &str { | ||
"zip" | ||
} | ||
|
||
fn signature(&self) -> Signature { | ||
Signature::build("zip").required( | ||
"block", | ||
SyntaxShape::Block, | ||
"the block to run and zip into the table", | ||
) | ||
} | ||
|
||
fn usage(&self) -> &str { | ||
"Zip two tables." | ||
} | ||
|
||
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> { | ||
command(args) | ||
} | ||
|
||
fn examples(&self) -> Vec<Example> { | ||
vec![Example { | ||
description: "Zip two lists", | ||
example: "[0 2 4 6 8] | zip { [1 3 5 7 9] } | each { $it }", | ||
result: None, | ||
}, | ||
Example { | ||
description: "Zip two tables", | ||
example: "[[symbol]; ['('] ['['] ['{']] | zip { [[symbol]; [')'] [']'] ['}']] } | each { get symbol | $'($in.0)nushell($in.1)' }", | ||
result: Some(vec![ | ||
Value::from("(nushell)"), | ||
Value::from("[nushell]"), | ||
Value::from("{nushell}") | ||
]) | ||
}] | ||
} | ||
} | ||
|
||
fn command(args: CommandArgs) -> Result<OutputStream, ShellError> { | ||
let context = &args.context; | ||
let name_tag = args.call_info.name_tag.clone(); | ||
|
||
let block: CapturedBlock = args.req(0)?; | ||
let block_span = &block.block.span.clone(); | ||
let input = args.input; | ||
|
||
context.scope.enter_scope(); | ||
context.scope.add_vars(&block.captured.entries); | ||
let result = run_block( | ||
&block.block, | ||
context, | ||
InputStream::empty(), | ||
ExternalRedirection::Stdout, | ||
); | ||
context.scope.exit_scope(); | ||
|
||
Ok(OutputStream::from_stream(zip( | ||
input, | ||
result, | ||
name_tag, | ||
*block_span, | ||
)?)) | ||
} | ||
|
||
fn zip<'a>( | ||
l: impl Iterator<Item = Value> + 'a + Sync + Send, | ||
r: Result<InputStream, ShellError>, | ||
command_tag: Tag, | ||
secondary_command_span: Span, | ||
) -> Result<Box<dyn Iterator<Item = Value> + 'a + Sync + Send>, ShellError> { | ||
Ok(Box::new(l.zip(r?).map(move |(s1, s2)| match (s1, s2) { | ||
( | ||
left_row | ||
@ | ||
Value { | ||
value: UntaggedValue::Row(_), | ||
.. | ||
}, | ||
mut | ||
right_row | ||
@ | ||
Value { | ||
value: UntaggedValue::Row(_), | ||
.. | ||
}, | ||
) => { | ||
let mut zipped_row = TaggedDictBuilder::new(left_row.tag()); | ||
|
||
right_row.tag = Tag::new(right_row.tag.anchor(), secondary_command_span); | ||
|
||
for column in left_row.data_descriptors() { | ||
let path = ColumnPath::build(&(column.to_string()).spanned(right_row.tag.span)); | ||
zipped_row.insert_value(column, zip_row(&path, &left_row, &right_row)); | ||
} | ||
|
||
zipped_row.into_value() | ||
} | ||
(s1, s2) => { | ||
let mut name_tag = command_tag.clone(); | ||
name_tag.anchor = s1.tag.anchor(); | ||
UntaggedValue::table(&vec![s1, s2]).into_value(&name_tag) | ||
} | ||
}))) | ||
} | ||
|
||
fn zip_row(path: &ColumnPath, left: &Value, right: &Value) -> UntaggedValue { | ||
UntaggedValue::table(&vec![ | ||
get_column(path, left) | ||
.unwrap_or_else(|err| UntaggedValue::Error(err).into_untagged_value()), | ||
get_column(path, right) | ||
.unwrap_or_else(|err| UntaggedValue::Error(err).into_untagged_value()), | ||
]) | ||
} | ||
|
||
pub fn get_column(path: &ColumnPath, value: &Value) -> Result<Value, ShellError> { | ||
get_data_by_column_path(value, path, move |obj_source, column_path_tried, error| { | ||
let path_members_span = path.maybe_span().unwrap_or_else(Span::unknown); | ||
|
||
if obj_source.is_row() { | ||
if let Some(error) = error_message(column_path_tried, &path_members_span, obj_source) { | ||
return error; | ||
} | ||
} | ||
|
||
error | ||
}) | ||
} | ||
|
||
fn error_message( | ||
column_tried: &PathMember, | ||
path_members_span: &Span, | ||
obj_source: &Value, | ||
) -> Option<ShellError> { | ||
match column_tried { | ||
PathMember { | ||
unspanned: UnspannedPathMember::String(column), | ||
.. | ||
} => { | ||
let primary_label = format!("There isn't a column named '{}' from this table", &column); | ||
|
||
did_you_mean(obj_source, column_tried.as_string()).map(|suggestions| { | ||
ShellError::labeled_error_with_secondary( | ||
"Unknown column", | ||
primary_label, | ||
obj_source.tag.span, | ||
format!( | ||
"Perhaps you meant '{}'? Columns available: {}", | ||
suggestions[0], | ||
&obj_source.data_descriptors().join(", ") | ||
), | ||
column_tried.span.since(path_members_span), | ||
) | ||
}) | ||
} | ||
_ => None, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,3 +63,4 @@ mod where_; | |
mod which; | ||
mod with_env; | ||
mod wrap; | ||
mod zip; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
use nu_test_support::fs::Stub::FileWithContent; | ||
use nu_test_support::pipeline as input; | ||
use nu_test_support::playground::{says, Playground}; | ||
|
||
use hamcrest2::assert_that; | ||
use hamcrest2::prelude::*; | ||
|
||
const ZIP_POWERED_TEST_ASSERTION_SCRIPT: &str = r#" | ||
def expect [ | ||
left, | ||
right, | ||
--to-eq | ||
] { | ||
$left | zip { $right } | all? { | ||
$it.name.0 == $it.name.1 && $it.commits.0 == $it.commits.1 | ||
} | ||
} | ||
def add-commits [n] { | ||
each { | ||
let contributor = $it; | ||
let name = $it.name; | ||
let commits = $it.commits; | ||
$contributor | merge { | ||
[[commits]; [($commits + $n)]] | ||
} | ||
} | ||
} | ||
"#; | ||
|
||
#[test] | ||
fn zips_two_tables() { | ||
Playground::setup("zip_test_1", |dirs, nu| { | ||
nu.with_files(vec![FileWithContent( | ||
"zip_test.nu", | ||
&format!("{}\n", ZIP_POWERED_TEST_ASSERTION_SCRIPT), | ||
)]); | ||
|
||
assert_that!( | ||
nu.pipeline(&input(&format!( | ||
r#" | ||
source {} ; | ||
let contributors = ([ | ||
[name, commits]; | ||
[andres, 10] | ||
[ jt, 20] | ||
]); | ||
let actual = ($contributors | add-commits 10); | ||
expect $actual --to-eq [[name, commits]; [andres, 20] [jt, 30]] | ||
"#, | ||
dirs.test().join("zip_test.nu").display() | ||
))), | ||
says().stdout("true") | ||
); | ||
}) | ||
} | ||
|
||
#[test] | ||
fn zips_two_lists() { | ||
Playground::setup("zip_test_2", |_, nu| { | ||
assert_that!( | ||
nu.pipeline(&input( | ||
r#" | ||
echo [0 2 4 6 8] | zip { [1 3 5 7 9] } | ||
| flatten | ||
| into string | ||
| str collect '-' | ||
"# | ||
)), | ||
says().stdout("0-1-2-3-4-5-6-7-8-9") | ||
); | ||
}) | ||
} |