diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index b82a77b545..74c1031055 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -85,6 +85,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { Skip, SkipUntil, SkipWhile, + SortBy, Transpose, Uniq, Update, diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index 0eb48165bc..64f84c071a 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -27,6 +27,7 @@ mod reverse; mod select; mod shuffle; mod skip; +mod sort_by; mod transpose; mod uniq; mod update; @@ -63,6 +64,7 @@ pub use reverse::Reverse; pub use select::Select; pub use shuffle::Shuffle; pub use skip::*; +pub use sort_by::SortBy; pub use transpose::Transpose; pub use uniq::*; pub use update::Update; diff --git a/crates/nu-command/src/filters/sort_by.rs b/crates/nu-command/src/filters/sort_by.rs new file mode 100644 index 0000000000..d84585dec2 --- /dev/null +++ b/crates/nu-command/src/filters/sort_by.rs @@ -0,0 +1,191 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct SortBy; + +impl Command for SortBy { + fn name(&self) -> &str { + "sort-by" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("sort-by") + .rest("columns", SyntaxShape::Any, "the column(s) to sort by") + .switch("reverse", "Sort in reverse order", Some('r')) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Sort by the given columns, in increasing order." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[2 0 1] | sort-by", + description: "sort the list by increasing value", + result: Some(Value::List { + vals: vec![Value::test_int(0), Value::test_int(1), Value::test_int(2)], + span: Span::test_data(), + }), + }, + Example { + example: "[2 0 1] | sort-by -r", + description: "sort the list by decreasing value", + result: Some(Value::List { + vals: vec![Value::test_int(2), Value::test_int(1), Value::test_int(0)], + span: Span::test_data(), + }), + }, + Example { + example: "[betty amy sarah] | sort-by", + description: "sort a list of strings", + result: Some(Value::List { + vals: vec![ + Value::test_string("amy"), + Value::test_string("betty"), + Value::test_string("sarah"), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[betty amy sarah] | sort-by -r", + description: "sort a list of strings in reverse", + result: Some(Value::List { + vals: vec![ + Value::test_string("sarah"), + Value::test_string("betty"), + Value::test_string("amy"), + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + let reverse = call.has_flag("reverse"); + let mut vec: Vec<_> = input.into_iter().collect(); + + sort(&mut vec, columns, call)?; + + if reverse { + vec.reverse() + } + + let iter = vec.into_iter(); + Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())) + } +} + +pub fn sort(vec: &mut [Value], columns: Vec, call: &Call) -> Result<(), ShellError> { + match &vec[0] { + Value::Record { + cols: _cols, + vals: _input_vals, + .. + } => { + vec.sort_by(|a, b| { + process(a, b, &columns[0], call) + .expect("sort_by Value::Record bug") + .compare() + }); + } + _ => { + vec.sort_by(|a, b| coerce_compare(a, b).expect("sort_by default bug").compare()); + } + } + Ok(()) +} + +pub fn process( + left: &Value, + right: &Value, + column: &str, + call: &Call, +) -> Result { + let left_value = left.get_data_by_key(column); + + let left_res = match left_value { + Some(left_res) => left_res, + None => Value::Nothing { span: call.head }, + }; + + let right_value = right.get_data_by_key(column); + + let right_res = match right_value { + Some(right_res) => right_res, + None => Value::Nothing { span: call.head }, + }; + + coerce_compare(&left_res, &right_res) +} + +#[derive(Debug)] +pub enum CompareValues { + Ints(i64, i64), + // Floats(f64, f64), + String(String, String), + Booleans(bool, bool), +} + +impl CompareValues { + pub fn compare(&self) -> std::cmp::Ordering { + match self { + CompareValues::Ints(left, right) => left.cmp(right), + // f64: std::cmp::Ord is required + // CompareValues::Floats(left, right) => left.cmp(right), + CompareValues::String(left, right) => left.cmp(right), + CompareValues::Booleans(left, right) => left.cmp(right), + } + } +} + +pub fn coerce_compare( + left: &Value, + right: &Value, +) -> Result { + match (left, right) { + /* + (Value::Float { val: left, .. }, Value::Float { val: right, .. }) => { + Ok(CompareValues::Floats(*left, *right)) + } + */ + (Value::Int { val: left, .. }, Value::Int { val: right, .. }) => { + Ok(CompareValues::Ints(*left, *right)) + } + (Value::String { val: left, .. }, Value::String { val: right, .. }) => { + Ok(CompareValues::String(left.clone(), right.clone())) + } + (Value::Bool { val: left, .. }, Value::Bool { val: right, .. }) => { + Ok(CompareValues::Booleans(*left, *right)) + } + _ => Err(("coerce_compare_left", "coerce_compare_right")), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SortBy {}) + } +}