Unify sort and sort_by logic

This commit is contained in:
132ikl 2024-07-08 21:03:08 -04:00
parent f940edbee2
commit 83fbf393b0
3 changed files with 138 additions and 274 deletions

View File

@ -1,9 +1,12 @@
use alphanumeric_sort::compare_str; use alphanumeric_sort::compare_str;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_protocol::{ast::PathMember, IntoValue};
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::{compare_by, compare_values, Comparator};
#[derive(Clone)] #[derive(Clone)]
pub struct Sort; pub struct Sort;
@ -14,10 +17,13 @@ impl Command for Sort {
fn signature(&self) -> nu_protocol::Signature { fn signature(&self) -> nu_protocol::Signature {
Signature::build("sort") Signature::build("sort")
.input_output_types(vec![( .input_output_types(vec![
Type::List(Box::new(Type::Any)), (
Type::List(Box::new(Type::Any)), Type::List(Box::new(Type::Any)),
), (Type::record(), Type::record()),]) Type::List(Box::new(Type::Any))
),
(Type::record(), Type::record())
])
.switch("reverse", "Sort in reverse order", Some('r')) .switch("reverse", "Sort in reverse order", Some('r'))
.switch( .switch(
"ignore-case", "ignore-case",
@ -134,233 +140,52 @@ impl Command for Sort {
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head;
let reverse = call.has_flag(engine_state, stack, "reverse")?; let reverse = call.has_flag(engine_state, stack, "reverse")?;
let insensitive = call.has_flag(engine_state, stack, "ignore-case")?; let insensitive = call.has_flag(engine_state, stack, "ignore-case")?;
let natural = call.has_flag(engine_state, stack, "natural")?; let natural = call.has_flag(engine_state, stack, "natural")?;
let sort_by_value = call.has_flag(engine_state, stack, "values")?;
let metadata = input.metadata(); let metadata = input.metadata();
let span = input.span().unwrap_or(call.head); let span = input.span().unwrap_or(call.head);
match input { let value = input.into_value(span)?;
// Records have two sorting methods, toggled by presence or absence of -v let sorted: Value = match value {
PipelineData::Value(Value::Record { val, .. }, ..) => { Value::Record { val, .. } => {
let sort_by_value = call.has_flag(engine_state, stack, "values")?; // Records have two sorting methods, toggled by presence or absence of -v
let record = sort_record( let record = crate::sort_record(
val.into_owned(), val.into_owned(),
span,
sort_by_value, sort_by_value,
reverse, reverse,
insensitive, insensitive,
natural, natural,
); )?;
Ok(record.into_pipeline_data()) Value::record(record, span)
} }
// Other values are sorted here Value::List { vals, .. } => {
PipelineData::Value(v, ..) let mut vec = vals.to_owned();
if !matches!(v, Value::List { .. } | Value::Range { .. }) =>
{
Ok(v.into_pipeline_data())
}
pipe_data => {
let mut vec: Vec<_> = pipe_data.into_iter().collect();
sort(&mut vec, head, insensitive, natural)?; crate::sort(&mut vec, insensitive, natural)?;
if reverse { if reverse {
vec.reverse() vec.reverse()
} }
let iter = vec.into_iter(); Value::list(vec, span)
Ok(iter.into_pipeline_data_with_metadata(
head,
engine_state.ctrlc.clone(),
metadata,
))
} }
} Value::Nothing { .. } => {
} return Err(ShellError::PipelineEmpty {
} dst_span: value.span(),
})
fn sort_record(
record: Record,
rec_span: Span,
sort_by_value: bool,
reverse: bool,
insensitive: bool,
natural: bool,
) -> Value {
let mut input_pairs: Vec<(String, Value)> = record.into_iter().collect();
input_pairs.sort_by(|a, b| {
// Extract the data (if sort_by_value) or the column names for comparison
let left_res = if sort_by_value {
match &a.1 {
Value::String { val, .. } => val.clone(),
val => {
if let Ok(val) = val.coerce_string() {
val
} else {
// Values that can't be turned to strings are disregarded by the sort
// (same as in sort_utils.rs)
return Ordering::Equal;
}
}
} }
} else { _ => {
a.0.clone() return Err(ShellError::PipelineMismatch {
}; exp_input_type: "record or list".to_string(),
let right_res = if sort_by_value { dst_span: call.head,
match &b.1 { src_span: value.span(),
Value::String { val, .. } => val.clone(), })
val => {
if let Ok(val) = val.coerce_string() {
val
} else {
// Values that can't be turned to strings are disregarded by the sort
// (same as in sort_utils.rs)
return Ordering::Equal;
}
}
} }
} else {
b.0.clone()
}; };
Ok(sorted.into_pipeline_data_with_metadata(metadata))
// Fold case if case-insensitive
let left = if insensitive {
left_res.to_folded_case()
} else {
left_res
};
let right = if insensitive {
right_res.to_folded_case()
} else {
right_res
};
if natural {
compare_str(left, right)
} else {
left.cmp(&right)
}
});
if reverse {
input_pairs.reverse();
} }
Value::record(input_pairs.into_iter().collect(), rec_span)
}
pub fn sort(
vec: &mut [Value],
span: Span,
insensitive: bool,
natural: bool,
) -> Result<(), ShellError> {
match vec.first() {
Some(Value::Record { val, .. }) => {
let columns: Vec<String> = val.columns().cloned().collect();
vec.sort_by(|a, b| process(a, b, &columns, span, insensitive, natural));
}
_ => {
vec.sort_by(|a, b| {
let span_a = a.span();
let span_b = b.span();
if insensitive {
let folded_left = match a {
Value::String { val, .. } => Value::string(val.to_folded_case(), span_a),
_ => a.clone(),
};
let folded_right = match b {
Value::String { val, .. } => Value::string(val.to_folded_case(), span_b),
_ => b.clone(),
};
if natural {
match (
folded_left.coerce_into_string(),
folded_right.coerce_into_string(),
) {
(Ok(left), Ok(right)) => compare_str(left, right),
_ => Ordering::Equal,
}
} else {
folded_left
.partial_cmp(&folded_right)
.unwrap_or(Ordering::Equal)
}
} else if natural {
match (a.coerce_str(), b.coerce_str()) {
(Ok(left), Ok(right)) => compare_str(left, right),
_ => 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,
natural: 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 span_left = left_res.span();
let span_right = right_res.span();
let folded_left = match left_res {
Value::String { val, .. } => Value::string(val.to_folded_case(), span_left),
_ => left_res,
};
let folded_right = match right_res {
Value::String { val, .. } => Value::string(val.to_folded_case(), span_right),
_ => right_res,
};
if natural {
match (
folded_left.coerce_into_string(),
folded_right.coerce_into_string(),
) {
(Ok(left), Ok(right)) => compare_str(left, right),
_ => Ordering::Equal,
}
} else {
folded_left
.partial_cmp(&folded_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)] #[cfg(test)]

View File

@ -125,7 +125,7 @@ impl Command for SortBy {
} }
} }
crate::sort(&mut vec, comparators, head, insensitive, natural)?; crate::sort_by(&mut vec, comparators, head, insensitive, natural)?;
if reverse { if reverse {
vec.reverse() vec.reverse()

View File

@ -3,7 +3,7 @@ use nu_engine::ClosureEval;
use nu_protocol::{ use nu_protocol::{
ast::CellPath, ast::CellPath,
engine::{Closure, EngineState, Stack}, engine::{Closure, EngineState, Stack},
PipelineData, ShellError, Span, Value, PipelineData, Record, ShellError, Span, Value,
}; };
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use std::cmp::Ordering; use std::cmp::Ordering;
@ -17,56 +17,33 @@ pub enum Comparator {
CellPath(CellPath), CellPath(CellPath),
} }
/// Sort a value. This only makes sense for lists and list-like things, pub fn sort(vec: &mut [Value], insensitive: bool, natural: bool) -> Result<(), ShellError> {
/// so for everything else we just return the value as-is. // to apply insensitive or natural sorting, all values must be strings
/// CustomValues are converted to their base value and then sorted. let string_sort: bool = vec
pub fn sort_value( .iter()
val: &Value, .all(|value| matches!(value, &Value::String { .. }));
comparators: Vec<Comparator>,
ascending: bool,
insensitive: bool,
natural: bool,
) -> Result<Value, ShellError> {
let span = val.span();
match val {
Value::List { vals, .. } => {
let mut vals = vals.clone();
sort(&mut vals, comparators, span, insensitive, natural)?;
if !ascending { // allow the comparator function to indicate error
vals.reverse(); // by mutating this option captured by the closure,
} // since sort_by closure must be infallible
let mut compare_err: Option<ShellError> = None;
Ok(Value::list(vals, span)) vec.sort_by(|a, b| {
} crate::compare_values(a, b, insensitive && string_sort, natural && string_sort)
Value::Custom { val, .. } => { .unwrap_or_else(|err| {
let base_val = val.to_base_value(span)?; compare_err.get_or_insert(err);
sort_value(&base_val, comparators, ascending, insensitive, natural) Ordering::Equal
} })
_ => Ok(val.to_owned()), });
if let Some(err) = compare_err {
Err(err)
} else {
Ok(())
} }
} }
/// Sort a value in-place. This is more efficient than sort_value() because it pub fn sort_by(
/// avoids cloning, but it does not work for CustomValues; they are returned as-is.
pub fn sort_value_in_place(
val: &mut Value,
comparators: Vec<Comparator>,
ascending: bool,
insensitive: bool,
natural: bool,
) -> Result<(), ShellError> {
let span = val.span();
if let Value::List { vals, .. } = val {
sort(vals, comparators, span, insensitive, natural)?;
if !ascending {
vals.reverse();
}
}
Ok(())
}
pub fn sort(
vec: &mut [Value], vec: &mut [Value],
comparators: Vec<Comparator>, comparators: Vec<Comparator>,
span: Span, span: Span,
@ -102,7 +79,7 @@ pub fn sort(
let mut compare_err: Option<ShellError> = None; let mut compare_err: Option<ShellError> = None;
vec.sort_by(|a, b| { vec.sort_by(|a, b| {
compare( compare_by(
a, a,
b, b,
&comparators, &comparators,
@ -120,7 +97,43 @@ pub fn sort(
} }
} }
pub fn compare( pub fn sort_record(
record: Record,
sort_by_value: bool,
reverse: bool,
insensitive: bool,
natural: bool,
) -> Result<Record, ShellError> {
let mut input_pairs: Vec<(String, Value)> = record.into_iter().collect();
// allow the comparator function to indicate error
// by mutating this option captured by the closure,
// since sort_by closure must be infallible
let mut compare_err: Option<ShellError> = None;
input_pairs.sort_by(|a, b| {
if sort_by_value {
compare_values(&a.1, &b.1, insensitive, natural).unwrap_or_else(|err| {
compare_err.get_or_insert(err);
Ordering::Equal
})
} else {
compare_strings(&a.0, &b.0, insensitive, natural)
}
});
if reverse {
input_pairs.reverse()
}
if let Some(err) = compare_err {
return Err(err);
}
Ok(input_pairs.into_iter().collect())
}
pub fn compare_by(
left: &Value, left: &Value,
right: &Value, right: &Value,
comparators: &[Comparator], comparators: &[Comparator],
@ -153,6 +166,48 @@ pub fn compare(
Ordering::Equal Ordering::Equal
} }
pub fn compare_values(
left: &Value,
right: &Value,
insensitive: bool,
natural: bool,
) -> Result<Ordering, ShellError> {
if insensitive || natural {
let left_str = left.coerce_string()?;
let right_str = right.coerce_string()?;
Ok(compare_strings(&left_str, &right_str, insensitive, natural))
} else {
Ok(left.partial_cmp(&right).unwrap_or(Ordering::Equal))
}
}
pub fn compare_strings(
left: &String,
right: &String,
insensitive: bool,
natural: bool,
) -> Ordering {
// declare these names now to appease compiler
// not needed in nightly, but needed as of 1.77.2, so can be removed later
let (left_copy, right_copy);
// only allocate new String if necessary for case folding,
// so callers don't need to pass an owned String
let (left_str, right_str) = if insensitive {
left_copy = left.to_folded_case();
right_copy = right.to_folded_case();
(&left_copy, &right_copy)
} else {
(left, right)
};
if natural {
alphanumeric_sort::compare_str(left_str, right_str)
} else {
left_str.partial_cmp(right_str).unwrap_or(Ordering::Equal)
}
}
pub fn compare_cell_path( pub fn compare_cell_path(
left: &Value, left: &Value,
right: &Value, right: &Value,
@ -162,23 +217,7 @@ pub fn compare_cell_path(
) -> Result<Ordering, ShellError> { ) -> Result<Ordering, ShellError> {
let left = left.clone().follow_cell_path(&cell_path.members, false)?; let left = left.clone().follow_cell_path(&cell_path.members, false)?;
let right = right.clone().follow_cell_path(&cell_path.members, false)?; let right = right.clone().follow_cell_path(&cell_path.members, false)?;
compare_values(&left, &right, insensitive, natural)
if insensitive || natural {
let mut left_str = left.coerce_into_string()?;
let mut right_str = right.coerce_into_string()?;
if insensitive {
left_str = left_str.to_folded_case();
right_str = right_str.to_folded_case();
}
if natural {
Ok(compare_str(left_str, right_str))
} else {
Ok(left_str.partial_cmp(&right_str).unwrap_or(Ordering::Equal))
}
} else {
Ok(left.partial_cmp(&right).unwrap_or(Ordering::Equal))
}
} }
pub fn compare_closure( pub fn compare_closure(