diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 831dc0e649..9566305760 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -106,6 +106,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { Skip, SkipUntil, SkipWhile, + Sort, SortBy, Transpose, Uniq, diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index ca8b184a37..ac4937ee18 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -35,6 +35,7 @@ mod rotate; mod select; mod shuffle; mod skip; +mod sort; mod sort_by; mod split_by; mod transpose; @@ -84,6 +85,7 @@ pub use rotate::Rotate; pub use select::Select; pub use shuffle::Shuffle; pub use skip::*; +pub use sort::Sort; pub use sort_by::SortBy; pub use split_by::SplitBy; pub use transpose::Transpose; diff --git a/crates/nu-command/src/filters/sort.rs b/crates/nu-command/src/filters/sort.rs new file mode 100644 index 0000000000..cd850821fe --- /dev/null +++ b/crates/nu-command/src/filters/sort.rs @@ -0,0 +1,239 @@ +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + Value, +}; +use std::cmp::Ordering; + +#[derive(Clone)] +pub struct Sort; + +impl Command for Sort { + fn name(&self) -> &str { + "sort" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("sort-by") + .switch("reverse", "Sort in reverse order", Some('r')) + .switch( + "insensitive", + "Sort string-based columns case-insensitively", + Some('i'), + ) + .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", + 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 -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", + 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 -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(), + }), + }, + Example { + description: "Sort strings (case-insensitive)", + example: "echo [airplane Truck Car] | sort -i", + result: Some(Value::List { + vals: vec![ + Value::test_string("airplane"), + Value::test_string("Car"), + Value::test_string("Truck"), + ], + span: Span::test_data(), + }), + }, + Example { + description: "Sort strings (reversed case-insensitive)", + example: "echo [airplane Truck Car] | sort -i -r", + result: Some(Value::List { + vals: vec![ + Value::test_string("Truck"), + Value::test_string("Car"), + Value::test_string("airplane"), + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let reverse = call.has_flag("reverse"); + let insensitive = call.has_flag("insensitive"); + let metadata = &input.metadata(); + let mut vec: Vec<_> = input.into_iter().collect(); + + sort(&mut vec, call.head, insensitive)?; + + if reverse { + vec.reverse() + } + + let iter = vec.into_iter(); + match &*metadata { + Some(m) => { + Ok(iter.into_pipeline_data_with_metadata(m.clone(), engine_state.ctrlc.clone())) + } + None => Ok(iter.into_pipeline_data(engine_state.ctrlc.clone())), + } + } +} + +pub fn sort(vec: &mut [Value], span: Span, insensitive: bool) -> Result<(), ShellError> { + if vec.is_empty() { + return Err(ShellError::LabeledError( + "no values to work with".to_string(), + "no values to work with".to_string(), + )); + } + + match &vec[0] { + Value::Record { + cols, + vals: _input_vals, + .. + } => { + let columns = cols.clone(); + vec.sort_by(|a, b| process(a, b, &columns, span, insensitive)); + } + _ => { + vec.sort_by(|a, b| { + if insensitive { + let lowercase_left = match a { + Value::String { val, span } => Value::String { + val: val.to_ascii_lowercase(), + span: *span, + }, + _ => a.clone(), + }; + + let lowercase_right = match b { + Value::String { val, span } => Value::String { + val: val.to_ascii_lowercase(), + span: *span, + }, + _ => b.clone(), + }; + + lowercase_left + .partial_cmp(&lowercase_right) + .unwrap_or(Ordering::Equal) + } else { + a.partial_cmp(b).unwrap_or(Ordering::Equal) + } + }); + } + } + Ok(()) +} + +pub fn process( + left: &Value, + right: &Value, + columns: &[String], + span: Span, + insensitive: bool, +) -> Ordering { + for column in columns { + let left_value = left.get_data_by_key(column); + + let left_res = match left_value { + Some(left_res) => left_res, + None => Value::Nothing { span }, + }; + + let right_value = right.get_data_by_key(column); + + let right_res = match right_value { + Some(right_res) => right_res, + None => Value::Nothing { span }, + }; + + let result = if insensitive { + let lowercase_left = match left_res { + Value::String { val, span } => Value::String { + val: val.to_ascii_lowercase(), + span, + }, + _ => left_res, + }; + + let lowercase_right = match right_res { + Value::String { val, span } => Value::String { + val: val.to_ascii_lowercase(), + span, + }, + _ => right_res, + }; + lowercase_left + .partial_cmp(&lowercase_right) + .unwrap_or(Ordering::Equal) + } else { + left_res.partial_cmp(&right_res).unwrap_or(Ordering::Equal) + }; + if result != Ordering::Equal { + return result; + } + } + + Ordering::Equal +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Sort {}) + } +}