90 degree table rotations (clockwise and counter-clockwise) (#3086)

Also for 180 degree is expected. Rotation is not exactly like pivoting (transposing)
for instance, given the following table:

```
> echo [[col1, col2, col3]; [cell1, cell2, cell3] [cell4, cell5, cell6]]
───┬───────┬───────┬───────
 # │ col1  │ col2  │ col3
───┼───────┼───────┼───────
 0 │ cell1 │ cell2 │ cell3
 1 │ cell4 │ cell5 │ cell6
───┴───────┴───────┴───────
```

To rotate it counter clockwise by 90 degrees, we can resort to first transposing (`pivot`)
them adding a new column (preferably integers), sort by that column from highest to lowest,
then remove the column and we have a counter clockwise rotation.

```
> echo [[col1, col2, col3]; [cell1, cell2, cell3] [cell4, cell5, cell6]] | pivot | each --numbered { = $it.item | insert idx $it.index } | sort-by idx | reverse | reject idx
───┬─────────┬─────────┬─────────
 # │ Column0 │ Column1 │ Column2
───┼─────────┼─────────┼─────────
 0 │ col3    │ cell3   │ cell6
 1 │ col2    │ cell2   │ cell5
 2 │ col1    │ cell1   │ cell4
───┴─────────┴─────────┴─────────
```

Which we can get easily, in this case, by doing:

```
> echo [[col1, col2, cel3]; [cell1, cell2, cell3] [cell4, cell5, cell6]] | rotate counter-clockwise
───┬─────────┬─────────┬─────────
 # │ Column0 │ Column1 │ Column2
───┼─────────┼─────────┼─────────
 0 │ col3    │ cell3   │ cell6
 1 │ col2    │ cell2   │ cell5
 2 │ col1    │ cell1   │ cell4
───┴─────────┴─────────┴─────────
```

There are also many powerful use cases with rotation, it makes a breeze creating tables with many columns, say:

```
echo 0..12 | rotate counter-clockwise | reject Column0
───┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬──────────┬──────────┬──────────┬──────────
 # │ Column1 │ Column2 │ Column3 │ Column4 │ Column5 │ Column6 │ Column7 │ Column8 │ Column9 │ Column10 │ Column11 │ Column12 │ Column13
───┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼──────────┼──────────┼──────────┼──────────
 0 │       0 │       1 │       2 │       3 │       4 │       5 │       6 │       7 │       8 │        9 │       10 │       11 │       12
───┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴──────────┴──────────┴──────────┴──────────
```
This commit is contained in:
Andrés N. Robalino 2021-02-22 05:56:34 -06:00 committed by GitHub
parent 42d18d2294
commit 803826cdcd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 331 additions and 0 deletions

View file

@ -98,6 +98,7 @@ pub(crate) mod reject;
pub(crate) mod rename;
pub(crate) mod reverse;
pub(crate) mod rm;
pub(crate) mod rotate;
pub(crate) mod run_external;
pub(crate) mod save;
pub(crate) mod select;
@ -244,6 +245,7 @@ pub(crate) use reject::Reject;
pub(crate) use rename::Rename;
pub(crate) use reverse::Reverse;
pub(crate) use rm::Remove;
pub(crate) use rotate::{Rotate, RotateCounterClockwise};
pub(crate) use run_external::RunExternalCommand;
pub(crate) use save::Save;
pub(crate) use select::Command as Select;

View file

@ -161,6 +161,8 @@ pub fn create_default_context(interactive: bool) -> Result<EvaluationContext, Bo
whole_stream_command(Pivot),
whole_stream_command(Headers),
whole_stream_command(Reduce),
whole_stream_command(Rotate),
whole_stream_command(RotateCounterClockwise),
// Data processing
whole_stream_command(Histogram),
whole_stream_command(Autoenv),

View file

@ -0,0 +1,121 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{
merge_descriptors, ColumnPath, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder,
UntaggedValue,
};
use nu_source::{SpannedItem, Tagged};
use nu_value_ext::ValueExt;
pub struct Command;
#[derive(Deserialize)]
pub struct Arguments {
rest: Vec<Tagged<String>>,
}
#[async_trait]
impl WholeStreamCommand for Command {
fn name(&self) -> &str {
"rotate"
}
fn signature(&self) -> Signature {
Signature::build("rotate").rest(
SyntaxShape::String,
"the names to give columns once rotated",
)
}
fn usage(&self) -> &str {
"Rotates the table by 90 degrees clockwise."
}
async fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
rotate(args).await
}
}
pub async fn rotate(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name = args.call_info.name_tag.clone();
let (Arguments { rest }, input) = args.process().await?;
let input = input.into_vec().await;
let total_rows = input.len();
let descs = merge_descriptors(&input);
let total_descriptors = descs.len();
let descs = descs.into_iter().rev().collect::<Vec<_>>();
if total_rows == 0 {
return Ok(OutputStream::empty());
}
let mut headers: Vec<String> = vec![];
for i in 0..=total_rows {
headers.push(format!("Column{}", i));
}
let first = input[0].clone();
let name = if first.tag.anchor().is_some() {
first.tag
} else {
name
};
let values =
UntaggedValue::table(&input.into_iter().rev().collect::<Vec<_>>()).into_value(&name);
let values = nu_data::utils::group(
&values,
&Some(Box::new(move |row_number: usize, _| {
Ok(match headers.get(row_number) {
Some(name) => name.clone(),
None => String::new(),
})
})),
&name,
)?;
Ok(futures::stream::iter(
(0..total_descriptors)
.map(move |row_number| {
let mut row = TaggedDictBuilder::new(&name);
for (current_numbered_column, (column_name, _)) in values.row_entries().enumerate()
{
let raw_column_path =
format!("{}.0.{}", column_name, descs[row_number]).spanned_unknown();
let path = ColumnPath::build(&raw_column_path);
match &values.get_data_by_column_path(&path, Box::new(move |_, _, error| error))
{
Ok(x) => {
row.insert_value(
rest.get(current_numbered_column)
.map(|c| c.item.clone())
.unwrap_or_else(|| column_name.to_string()),
x.clone(),
);
}
Err(_) => {}
}
}
row.insert_value(
rest.get(total_rows)
.map(|c| c.item.clone())
.unwrap_or_else(|| format!("Column{}", total_rows)),
UntaggedValue::string(&descs[row_number]).into_untagged_value(),
);
ReturnSuccess::value(row.into_value())
})
.rev()
.collect::<Vec<_>>(),
)
.to_output_stream())
}

View file

@ -0,0 +1,117 @@
use crate::prelude::*;
use nu_engine::WholeStreamCommand;
use nu_errors::ShellError;
use nu_protocol::{
merge_descriptors, ColumnPath, ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder,
UntaggedValue,
};
use nu_source::{SpannedItem, Tagged};
use nu_value_ext::ValueExt;
pub struct SubCommand;
#[derive(Deserialize)]
pub struct Arguments {
rest: Vec<Tagged<String>>,
}
#[async_trait]
impl WholeStreamCommand for SubCommand {
fn name(&self) -> &str {
"rotate counter-clockwise"
}
fn signature(&self) -> Signature {
Signature::build("rotate counter-clockwise").rest(
SyntaxShape::String,
"the names to give columns once rotated",
)
}
fn usage(&self) -> &str {
"Rotates the table by 90 degrees counter clockwise."
}
async fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
rotate(args).await
}
}
pub async fn rotate(args: CommandArgs) -> Result<OutputStream, ShellError> {
let name = args.call_info.name_tag.clone();
let (Arguments { rest }, input) = args.process().await?;
let input = input.into_vec().await;
let descs = merge_descriptors(&input);
let total_rows = input.len();
if total_rows == 0 {
return Ok(OutputStream::empty());
}
let mut headers: Vec<String> = vec![];
for i in 0..=total_rows {
headers.push(format!("Column{}", i + 1));
}
let first = input[0].clone();
let name = if first.tag.anchor().is_some() {
first.tag
} else {
name
};
let values = UntaggedValue::table(&input).into_value(&name);
let values = nu_data::utils::group(
&values,
&Some(Box::new(move |row_number: usize, _| {
Ok(match headers.get(row_number) {
Some(name) => name.clone(),
None => String::new(),
})
})),
&name,
)?;
Ok(futures::stream::iter(
(0..descs.len())
.rev()
.map(move |row_number| {
let mut row = TaggedDictBuilder::new(&name);
row.insert_value(
rest.get(0)
.map(|c| c.item.clone())
.unwrap_or_else(|| String::from("Column0")),
UntaggedValue::string(descs.get(row_number).unwrap_or(&String::new()))
.into_untagged_value(),
);
for (current_numbered_column, (column_name, _)) in values.row_entries().enumerate()
{
let raw_column_path =
format!("{}.0.{}", column_name, &descs[row_number]).spanned_unknown();
let path = ColumnPath::build(&raw_column_path);
match &values.get_data_by_column_path(&path, Box::new(move |_, _, error| error))
{
Ok(x) => {
row.insert_value(
rest.get(current_numbered_column + 1)
.map(|c| c.item.clone())
.unwrap_or_else(|| column_name.to_string()),
x.clone(),
);
}
Err(_) => {}
}
}
ReturnSuccess::value(row.into_value())
})
.collect::<Vec<_>>(),
)
.to_output_stream())
}

View file

@ -0,0 +1,5 @@
mod command;
mod counter_clockwise;
pub use command::Command as Rotate;
pub use counter_clockwise::SubCommand as RotateCounterClockwise;

View file

@ -43,6 +43,7 @@ mod reduce;
mod rename;
mod reverse;
mod rm;
mod rotate;
mod save;
mod select;
mod semicolon;

View file

@ -0,0 +1,83 @@
use nu_test_support::{nu, pipeline};
#[test]
fn counter_clockwise() {
let table = pipeline(
r#"
echo [
[col1, col2, EXPECTED];
[---, "|||", XX1]
[---, "|||", XX2]
[---, "|||", XX3]
]
"#,
);
let expected = nu!(cwd: ".", pipeline(
r#"
echo [
[ Column0, Column1, Column2, Column3];
[ EXPECTED, XX1, XX2, XX3]
[ col2, "|||", "|||", "|||"]
[ col1, ---, ---, ---]
]
| where Column0 == EXPECTED
| get Column1 Column2 Column3
| str collect "-"
"#,
));
let actual = nu!(
cwd: ".",
format!("{} | {}", table, pipeline(r#"
rotate counter-clockwise
| where Column0 == EXPECTED
| get Column1 Column2 Column3
| str collect "-"
"#)));
assert_eq!(actual.out, expected.out);
}
#[test]
fn clockwise() {
let table = pipeline(
r#"
echo [
[col1, col2, EXPECTED];
[ ---, "|||", XX1]
[ ---, "|||", XX2]
[ ---, "|||", XX3]
]
"#,
);
let expected = nu!(cwd: ".", pipeline(
r#"
echo [
[ Column0, Column1, Column2, Column3];
[ ---, ---, ---, col1]
[ "|||", "|||", "|||", col2]
[ XX3, XX2, XX1, EXPECTED]
]
| where Column3 == EXPECTED
| get Column0 Column1 Column2
| str collect "-"
"#,
));
let actual = nu!(
cwd: ".",
format!("{} | {}", table, pipeline(r#"
rotate
| where Column3 == EXPECTED
| get Column0 Column1 Column2
| str collect "-"
"#)));
assert_eq!(actual.out, expected.out);
}