mirror of
https://github.com/nushell/nushell
synced 2024-12-28 05:53:09 +00:00
Table content rolling. (#3097)
There are many use cases. Here we introduce the following: - The rows can be rolled `... | roll` (up) or `... | roll down` - Columns can be rolled too (the default is on the `left`, you can pass `... | roll column --opposite` to roll in the other direction) - You can `roll` the cells of a table and keeping the header names in the same order (`... | roll column --cells-only`) - Above examples can also be passed (Ex. `... | roll down 3`) a number to tell how many places to roll. Basic working example with rolling columns: ``` > echo '00000100' | split chars | each { str to-int } | rotate counter-clockwise _ | reject _ | rename bit1 bit2 bit3 bit4 bit5 bit6 bit7 bit8 ───┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬────── # │ bit1 │ bit2 │ bit3 │ bit4 │ bit5 │ bit6 │ bit7 │ bit8 ───┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼────── 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 ───┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴────── ``` We want to "shift" three bits to the left of the bitstring (four in decimal), let's try it: ``` > echo '00000100' | split chars | each { str to-int } | rotate counter-clockwise _ | reject _ | rename bit1 bit2 bit3 bit4 bit5 bit6 bit7 bit8 | roll column 3 ───┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬────── # │ bit4 │ bit5 │ bit6 │ bit7 │ bit8 │ bit1 │ bit2 │ bit3 ───┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼────── 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 ───┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴────── ``` The tables was rolled correctly (32 in decimal, for above bitstring). However, the *last three header names* look confusing. We can roll the cell contents only to fix it. ``` > echo '00000100' | split chars | each { str to-int } | rotate counter-clockwise _ | reject _ | rename bit1 bit2 bit3 bit4 bit5 bit6 bit7 bit8 | roll column 3 --cells-only ───┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬────── # │ bit1 │ bit2 │ bit3 │ bit4 │ bit5 │ bit6 │ bit7 │ bit8 ───┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼────── 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 ───┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴────── ``` There we go. Let's compute it's decimal value now (should be 32) ``` > echo '00000100' | split chars | each { str to-int } | rotate counter-clockwise _ | reject _ | roll column 3 --cells-only | pivot bit --ignore-titles | get bit | reverse | each --numbered { = $it.item * (2 ** $it.index) } | math sum 32 ```
This commit is contained in:
parent
23d8dc959c
commit
7a77910720
8 changed files with 435 additions and 0 deletions
|
@ -98,6 +98,7 @@ pub(crate) mod reject;
|
|||
pub(crate) mod rename;
|
||||
pub(crate) mod reverse;
|
||||
pub(crate) mod rm;
|
||||
pub(crate) mod roll;
|
||||
pub(crate) mod rotate;
|
||||
pub(crate) mod run_external;
|
||||
pub(crate) mod save;
|
||||
|
@ -245,6 +246,7 @@ pub(crate) use reject::Reject;
|
|||
pub(crate) use rename::Rename;
|
||||
pub(crate) use reverse::Reverse;
|
||||
pub(crate) use rm::Remove;
|
||||
pub(crate) use roll::{Roll, RollColumn, RollUp};
|
||||
pub(crate) use rotate::{Rotate, RotateCounterClockwise};
|
||||
pub(crate) use run_external::RunExternalCommand;
|
||||
pub(crate) use save::Save;
|
||||
|
|
|
@ -161,6 +161,9 @@ 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(Roll),
|
||||
whole_stream_command(RollColumn),
|
||||
whole_stream_command(RollUp),
|
||||
whole_stream_command(Rotate),
|
||||
whole_stream_command(RotateCounterClockwise),
|
||||
// Data processing
|
||||
|
|
117
crates/nu-command/src/commands/roll/column.rs
Normal file
117
crates/nu-command/src/commands/roll/column.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use crate::prelude::*;
|
||||
use nu_data::base::select_fields;
|
||||
use nu_engine::WholeStreamCommand;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value};
|
||||
use nu_source::Tagged;
|
||||
|
||||
use super::support::{rotate, Direction};
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Arguments {
|
||||
by: Option<Tagged<u64>>,
|
||||
opposite: bool,
|
||||
#[serde(rename(deserialize = "cells-only"))]
|
||||
cells_only: bool,
|
||||
}
|
||||
|
||||
impl Arguments {
|
||||
fn direction(&self) -> Direction {
|
||||
if self.opposite {
|
||||
return Direction::Left;
|
||||
}
|
||||
|
||||
Direction::Right
|
||||
}
|
||||
|
||||
fn move_headers(&self) -> bool {
|
||||
!self.cells_only
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"roll column"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("roll column")
|
||||
.optional("by", SyntaxShape::Int, "the number of times to roll")
|
||||
.switch("opposite", "roll in the opposite direction", Some('o'))
|
||||
.switch("cells-only", "only roll the cells", Some('c'))
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Rolls the table columns"
|
||||
}
|
||||
|
||||
async fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
roll(args).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn roll(args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
let (args, input) = args.process().await?;
|
||||
|
||||
Ok(input
|
||||
.map(move |value| {
|
||||
futures::stream::iter({
|
||||
let tag = value.tag();
|
||||
|
||||
roll_by(value, &args)
|
||||
.unwrap_or_else(|| vec![UntaggedValue::nothing().into_value(tag)])
|
||||
.into_iter()
|
||||
.map(ReturnSuccess::value)
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn roll_by(value: Value, options: &Arguments) -> Option<Vec<Value>> {
|
||||
let tag = value.tag();
|
||||
let direction = options.direction();
|
||||
|
||||
if value.is_row() {
|
||||
if options.move_headers() {
|
||||
let columns = value.data_descriptors();
|
||||
|
||||
if let Some(fields) = rotate(columns, &options.by, direction) {
|
||||
return Some(vec![select_fields(&value, &fields, &tag)]);
|
||||
}
|
||||
} else {
|
||||
let columns = value.data_descriptors();
|
||||
let values_rotated = rotate(
|
||||
value
|
||||
.row_entries()
|
||||
.map(|(_, value)| value)
|
||||
.map(Clone::clone)
|
||||
.collect::<Vec<_>>(),
|
||||
&options.by,
|
||||
direction,
|
||||
);
|
||||
|
||||
if let Some(ref values) = values_rotated {
|
||||
let mut out = TaggedDictBuilder::new(&tag);
|
||||
|
||||
for (k, v) in columns.iter().zip(values.iter()) {
|
||||
out.insert_value(k, v.clone());
|
||||
}
|
||||
|
||||
return Some(vec![out.into_value()]);
|
||||
}
|
||||
}
|
||||
None
|
||||
} else if value.is_table() {
|
||||
rotate(
|
||||
value.table_entries().map(Clone::clone).collect(),
|
||||
&options.by,
|
||||
direction,
|
||||
)
|
||||
} else {
|
||||
Some(vec![value])
|
||||
}
|
||||
}
|
52
crates/nu-command/src/commands/roll/command.rs
Normal file
52
crates/nu-command/src/commands/roll/command.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::prelude::*;
|
||||
use nu_engine::WholeStreamCommand;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use nu_source::Tagged;
|
||||
|
||||
use super::support::{rotate, Direction};
|
||||
|
||||
pub struct Command;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Arguments {
|
||||
by: Option<Tagged<u64>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for Command {
|
||||
fn name(&self) -> &str {
|
||||
"roll"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("roll").optional("by", SyntaxShape::Int, "the number of times to roll")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Rolls the table rows"
|
||||
}
|
||||
|
||||
async fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
roll(args).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn roll(args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
let name = args.call_info.name_tag.clone();
|
||||
let (args, mut input) = args.process().await?;
|
||||
|
||||
let values = input.drain_vec().await;
|
||||
|
||||
Ok(futures::stream::iter(
|
||||
roll_down(values, &args)
|
||||
.unwrap_or_else(|| vec![UntaggedValue::nothing().into_value(&name)])
|
||||
.into_iter()
|
||||
.map(ReturnSuccess::value),
|
||||
)
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn roll_down(values: Vec<Value>, Arguments { by: ref n }: &Arguments) -> Option<Vec<Value>> {
|
||||
rotate(values, n, Direction::Down)
|
||||
}
|
42
crates/nu-command/src/commands/roll/mod.rs
Normal file
42
crates/nu-command/src/commands/roll/mod.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
mod column;
|
||||
mod command;
|
||||
mod up;
|
||||
|
||||
pub use column::SubCommand as RollColumn;
|
||||
pub use command::Command as Roll;
|
||||
pub use up::SubCommand as RollUp;
|
||||
|
||||
mod support {
|
||||
|
||||
pub enum Direction {
|
||||
Left,
|
||||
Right,
|
||||
Down,
|
||||
Up,
|
||||
}
|
||||
|
||||
pub fn rotate<T: Clone>(
|
||||
mut collection: Vec<T>,
|
||||
n: &Option<nu_source::Tagged<u64>>,
|
||||
direction: Direction,
|
||||
) -> Option<Vec<T>> {
|
||||
if collection.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let values = collection.as_mut_slice();
|
||||
|
||||
let rotations = if let Some(n) = n {
|
||||
n.item as usize % values.len()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
match direction {
|
||||
Direction::Up | Direction::Right => values.rotate_left(rotations),
|
||||
Direction::Down | Direction::Left => values.rotate_right(rotations),
|
||||
}
|
||||
|
||||
Some(values.to_vec())
|
||||
}
|
||||
}
|
52
crates/nu-command/src/commands/roll/up.rs
Normal file
52
crates/nu-command/src/commands/roll/up.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::prelude::*;
|
||||
use nu_engine::WholeStreamCommand;
|
||||
use nu_errors::ShellError;
|
||||
use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value};
|
||||
use nu_source::Tagged;
|
||||
|
||||
use super::support::{rotate, Direction};
|
||||
|
||||
pub struct SubCommand;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Arguments {
|
||||
by: Option<Tagged<u64>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WholeStreamCommand for SubCommand {
|
||||
fn name(&self) -> &str {
|
||||
"roll up"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("roll up").optional("by", SyntaxShape::Int, "the number of times to roll")
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Rolls the table rows"
|
||||
}
|
||||
|
||||
async fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
roll(args).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn roll(args: CommandArgs) -> Result<OutputStream, ShellError> {
|
||||
let name = args.call_info.name_tag.clone();
|
||||
let (args, mut input) = args.process().await?;
|
||||
|
||||
let values = input.drain_vec().await;
|
||||
|
||||
Ok(futures::stream::iter(
|
||||
roll_up(values, &args)
|
||||
.unwrap_or_else(|| vec![UntaggedValue::nothing().into_value(&name)])
|
||||
.into_iter()
|
||||
.map(ReturnSuccess::value),
|
||||
)
|
||||
.to_output_stream())
|
||||
}
|
||||
|
||||
fn roll_up(values: Vec<Value>, Arguments { by: ref n }: &Arguments) -> Option<Vec<Value>> {
|
||||
rotate(values, n, Direction::Up)
|
||||
}
|
|
@ -43,6 +43,7 @@ mod reduce;
|
|||
mod rename;
|
||||
mod reverse;
|
||||
mod rm;
|
||||
mod roll;
|
||||
mod rotate;
|
||||
mod save;
|
||||
mod select;
|
||||
|
|
166
crates/nu-command/tests/commands/roll.rs
Normal file
166
crates/nu-command/tests/commands/roll.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use nu_test_support::{nu, pipeline};
|
||||
|
||||
mod rows {
|
||||
use super::*;
|
||||
|
||||
fn table() -> String {
|
||||
pipeline(
|
||||
r#"
|
||||
echo [
|
||||
[service, status];
|
||||
|
||||
[ruby, DOWN]
|
||||
[db, DOWN]
|
||||
[nud, DOWN]
|
||||
[expected, HERE]
|
||||
]"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roll_down_by_default() {
|
||||
let actual = nu!(
|
||||
cwd: ".",
|
||||
format!("{} | {}", table(), pipeline(r#"
|
||||
roll
|
||||
| first
|
||||
| get status
|
||||
"#)));
|
||||
|
||||
assert_eq!(actual.out, "HERE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_roll_up() {
|
||||
let actual = nu!(
|
||||
cwd: ".",
|
||||
format!("{} | {}", table(), pipeline(r#"
|
||||
roll up 3
|
||||
| first
|
||||
| get status
|
||||
"#)));
|
||||
|
||||
assert_eq!(actual.out, "HERE");
|
||||
}
|
||||
}
|
||||
|
||||
mod columns {
|
||||
use super::*;
|
||||
|
||||
fn table() -> String {
|
||||
pipeline(
|
||||
r#"
|
||||
echo [
|
||||
[commit_author, origin, stars];
|
||||
|
||||
[ "Andres", EC, amarillito]
|
||||
[ "Darren", US, black]
|
||||
[ "Jonathan", US, black]
|
||||
[ "Yehuda", US, black]
|
||||
[ "Jason", CA, gold]
|
||||
]"#,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roll_left_by_default() {
|
||||
let actual = nu!(
|
||||
cwd: ".",
|
||||
format!("{} | {}", table(), pipeline(r#"
|
||||
roll column
|
||||
| get
|
||||
| str collect "-"
|
||||
"#)));
|
||||
|
||||
assert_eq!(actual.out, "origin-stars-commit_author");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_roll_in_the_opposite_direction() {
|
||||
let actual = nu!(
|
||||
cwd: ".",
|
||||
format!("{} | {}", table(), pipeline(r#"
|
||||
roll column 2 --opposite
|
||||
| get
|
||||
| str collect "-"
|
||||
"#)));
|
||||
|
||||
assert_eq!(actual.out, "origin-stars-commit_author");
|
||||
}
|
||||
|
||||
struct ThirtieTwo<'a>(usize, &'a str);
|
||||
|
||||
#[test]
|
||||
fn can_roll_the_cells_only_keeping_the_header_names() {
|
||||
let four_bitstring = bitstring_to_nu_row_pipeline("00000100");
|
||||
let expected_value = ThirtieTwo(32, "bit1-bit2-bit3-bit4-bit5-bit6-bit7-bit8");
|
||||
|
||||
let actual = nu!(
|
||||
cwd: ".",
|
||||
format!("{} | roll column 3 --opposite --cells-only | get | str collect '-' ", four_bitstring)
|
||||
);
|
||||
|
||||
assert_eq!(actual.out, expected_value.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_in_bitstring_left_shifted_with_three_bits_should_be_32_in_decimal() {
|
||||
let four_bitstring = "00000100";
|
||||
let expected_value = ThirtieTwo(32, "00100000");
|
||||
|
||||
assert_eq!(
|
||||
shift_three_bits_to_the_left_to_bitstring(four_bitstring),
|
||||
expected_value.0.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
fn shift_three_bits_to_the_left_to_bitstring(bits: &str) -> String {
|
||||
// this pipeline takes the bitstring and outputs a nu row literal
|
||||
// for example the number 4 in bitstring:
|
||||
//
|
||||
// input: 00000100
|
||||
//
|
||||
// output:
|
||||
// [
|
||||
// [Column1, Column2, Column3, Column4, Column5, Column6, Column7, Column8];
|
||||
// [ 0, 0, 0, 0, 0, 1, 0, 0]
|
||||
// ]
|
||||
//
|
||||
let bitstring_as_nu_row_pipeline = bitstring_to_nu_row_pipeline(bits);
|
||||
|
||||
// this pipeline takes the nu bitstring row literal, computes it's
|
||||
// decimal value.
|
||||
let nu_row_literal_bitstring_to_decimal_value_pipeline = pipeline(
|
||||
r#"
|
||||
pivot bit --ignore-titles
|
||||
| get bit
|
||||
| reverse
|
||||
| each --numbered {
|
||||
= $it.item * (2 ** $it.index)
|
||||
}
|
||||
| math sum
|
||||
"#,
|
||||
);
|
||||
|
||||
nu!(
|
||||
cwd: ".",
|
||||
format!("{} | roll column 3 | {}", bitstring_as_nu_row_pipeline, nu_row_literal_bitstring_to_decimal_value_pipeline)
|
||||
).out
|
||||
}
|
||||
|
||||
fn bitstring_to_nu_row_pipeline(bits: &str) -> String {
|
||||
format!(
|
||||
"echo '{}' | {}",
|
||||
bits,
|
||||
pipeline(
|
||||
r#"
|
||||
split chars
|
||||
| each { str to-int }
|
||||
| rotate counter-clockwise _
|
||||
| reject _
|
||||
| rename bit1 bit2 bit3 bit4 bit5 bit6 bit7 bit8
|
||||
"#
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue