diff --git a/crates/nu-command/src/commands.rs b/crates/nu-command/src/commands.rs index a59edf6bfa..1785f074c1 100644 --- a/crates/nu-command/src/commands.rs +++ b/crates/nu-command/src/commands.rs @@ -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; diff --git a/crates/nu-command/src/commands/default_context.rs b/crates/nu-command/src/commands/default_context.rs index ddd947e981..4f0d801d19 100644 --- a/crates/nu-command/src/commands/default_context.rs +++ b/crates/nu-command/src/commands/default_context.rs @@ -161,6 +161,9 @@ pub fn create_default_context(interactive: bool) -> Result>, + 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 { + roll(args).await + } +} + +pub async fn roll(args: CommandArgs) -> Result { + 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> { + 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::>(), + &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]) + } +} diff --git a/crates/nu-command/src/commands/roll/command.rs b/crates/nu-command/src/commands/roll/command.rs new file mode 100644 index 0000000000..03581ec8ac --- /dev/null +++ b/crates/nu-command/src/commands/roll/command.rs @@ -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>, +} + +#[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 { + roll(args).await + } +} + +pub async fn roll(args: CommandArgs) -> Result { + 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, Arguments { by: ref n }: &Arguments) -> Option> { + rotate(values, n, Direction::Down) +} diff --git a/crates/nu-command/src/commands/roll/mod.rs b/crates/nu-command/src/commands/roll/mod.rs new file mode 100644 index 0000000000..a74a70df8b --- /dev/null +++ b/crates/nu-command/src/commands/roll/mod.rs @@ -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( + mut collection: Vec, + n: &Option>, + direction: Direction, + ) -> Option> { + 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()) + } +} diff --git a/crates/nu-command/src/commands/roll/up.rs b/crates/nu-command/src/commands/roll/up.rs new file mode 100644 index 0000000000..d371e176a6 --- /dev/null +++ b/crates/nu-command/src/commands/roll/up.rs @@ -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>, +} + +#[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 { + roll(args).await + } +} + +pub async fn roll(args: CommandArgs) -> Result { + 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, Arguments { by: ref n }: &Arguments) -> Option> { + rotate(values, n, Direction::Up) +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 2418212072..5f9d9a8385 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -43,6 +43,7 @@ mod reduce; mod rename; mod reverse; mod rm; +mod roll; mod rotate; mod save; mod select; diff --git a/crates/nu-command/tests/commands/roll.rs b/crates/nu-command/tests/commands/roll.rs new file mode 100644 index 0000000000..131855cf88 --- /dev/null +++ b/crates/nu-command/tests/commands/roll.rs @@ -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 + "# + ) + ) + } +}