This commit is contained in:
132ikl 2024-08-05 17:15:05 -04:00 committed by GitHub
commit 98f80af937
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 549 additions and 519 deletions

View File

@ -1,9 +1,5 @@
use alphanumeric_sort::compare_str;
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use nu_utils::IgnoreCaseExt;
use std::cmp::Ordering;
#[derive(Clone)] #[derive(Clone)]
pub struct Sort; pub struct Sort;
@ -14,10 +10,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::List(Box::new(Type::Any))
), (Type::record(), Type::record()),]) ),
(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 +133,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)?;
let sorted: Value = match value {
Value::Record { val, .. } => {
// Records have two sorting methods, toggled by presence or absence of -v // Records have two sorting methods, toggled by presence or absence of -v
PipelineData::Value(Value::Record { val, .. }, ..) => { let record = crate::sort_record(
let sort_by_value = call.has_flag(engine_state, stack, "values")?;
let record = 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.signals().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()
};
let right_res = if sort_by_value {
match &b.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 {
b.0.clone()
};
// 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| { return Err(ShellError::PipelineMismatch {
let span_a = a.span(); exp_input_type: "record or list".to_string(),
let span_b = b.span(); dst_span: call.head,
if insensitive { src_span: value.span(),
let folded_left = match a { })
Value::String { val, .. } => Value::string(val.to_folded_case(), span_a), }
_ => a.clone(),
}; };
Ok(sorted.into_pipeline_data_with_metadata(metadata))
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

@ -1,5 +1,7 @@
use nu_engine::command_prelude::*; use nu_engine::command_prelude::*;
use crate::Comparator;
#[derive(Clone)] #[derive(Clone)]
pub struct SortBy; pub struct SortBy;
@ -18,16 +20,23 @@ impl Command for SortBy {
(Type::record(), Type::table()), (Type::record(), Type::table()),
(Type::table(), Type::table()), (Type::table(), Type::table()),
]) ])
.rest("columns", SyntaxShape::Any, "The column(s) to sort by.") .rest(
"comparator",
SyntaxShape::OneOf(vec![
SyntaxShape::CellPath,
SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Any])),
]),
"The cell path(s) or closure(s) to compare elements by.",
)
.switch("reverse", "Sort in reverse order", Some('r')) .switch("reverse", "Sort in reverse order", Some('r'))
.switch( .switch(
"ignore-case", "ignore-case",
"Sort string-based columns case-insensitively", "Sort string-based data case-insensitively",
Some('i'), Some('i'),
) )
.switch( .switch(
"natural", "natural",
"Sort alphanumeric string-based columns naturally (1, 9, 10, 99, 100, ...)", "Sort alphanumeric string-based data naturally (1, 9, 10, 99, 100, ...)",
Some('n'), Some('n'),
) )
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
@ -79,21 +88,44 @@ impl Command for SortBy {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let columns: Vec<String> = call.rest(engine_state, stack, 0)?; let comparator_vals: Vec<Value> = call.rest(engine_state, stack, 0)?;
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 metadata = input.metadata(); let metadata = input.metadata();
let mut vec: Vec<_> = input.into_iter_strict(head)?.collect(); let mut vec: Vec<_> = input.into_iter_strict(head)?.collect();
if columns.is_empty() { if comparator_vals.is_empty() {
return Err(ShellError::MissingParameter { return Err(ShellError::MissingParameter {
param_name: "columns".into(), param_name: "comparator".into(),
span: head, span: head,
}); });
} }
crate::sort(&mut vec, columns, head, insensitive, natural)?; let mut comparators = vec![];
for val in comparator_vals.into_iter() {
match val {
Value::CellPath { val, .. } => {
comparators.push(Comparator::CellPath(val));
}
Value::Closure { val, .. } => {
comparators.push(Comparator::Closure(
*val,
engine_state.clone(),
stack.clone(),
));
}
_ => {
return Err(ShellError::TypeMismatch {
err_message:
"Cannot sort using a value which is not a cell path or closure".into(),
span: val.span(),
})
}
}
}
crate::sort_by(&mut vec, comparators, head, insensitive, natural)?;
if reverse { if reverse {
vec.reverse() vec.reverse()

View File

@ -1,6 +1,9 @@
use alphanumeric_sort::compare_str; use nu_engine::ClosureEval;
use nu_engine::column::nonexistent_column; use nu_protocol::{
use nu_protocol::{ShellError, Span, Value}; ast::CellPath,
engine::{Closure, EngineState, Stack},
PipelineData, Record, ShellError, Span, Value,
};
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use std::cmp::Ordering; use std::cmp::Ordering;
@ -8,344 +11,521 @@ use std::cmp::Ordering;
// Eventually it would be nice to find a better home for it; sorting logic is only coupled // Eventually it would be nice to find a better home for it; sorting logic is only coupled
// to commands for historical reasons. // to commands for historical reasons.
/// Sort a value. This only makes sense for lists and list-like things, pub enum Comparator {
/// so for everything else we just return the value as-is. Closure(Closure, EngineState, Stack),
/// CustomValues are converted to their base value and then sorted. CellPath(CellPath),
pub fn sort_value(
val: &Value,
sort_columns: Vec<String>,
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, sort_columns, span, insensitive, natural)?;
if !ascending {
vals.reverse();
} }
Ok(Value::list(vals, span)) pub fn sort(vec: &mut [Value], insensitive: bool, natural: bool) -> Result<(), ShellError> {
} // to apply insensitive or natural sorting, all values must be strings
Value::Custom { val, .. } => { let string_sort: bool = vec
let base_val = val.to_base_value(span)?; .iter()
sort_value(&base_val, sort_columns, ascending, insensitive, natural) .all(|value| matches!(value, &Value::String { .. }));
}
_ => Ok(val.to_owned()),
}
}
/// Sort a value in-place. This is more efficient than sort_value() because it // allow the comparator function to indicate error
/// avoids cloning, but it does not work for CustomValues; they are returned as-is. // by mutating this option captured by the closure,
pub fn sort_value_in_place( // since sort_by closure must be infallible
val: &mut Value, let mut compare_err: Option<ShellError> = None;
sort_columns: Vec<String>,
ascending: bool, vec.sort_by(|a, b| {
insensitive: bool, crate::compare_values(a, b, insensitive && string_sort, natural && string_sort)
natural: bool, .unwrap_or_else(|err| {
) -> Result<(), ShellError> { compare_err.get_or_insert(err);
let span = val.span(); Ordering::Equal
if let Value::List { vals, .. } = val { })
sort(vals, sort_columns, span, insensitive, natural)?; });
if !ascending {
vals.reverse(); if let Some(err) = compare_err {
} Err(err)
} } else {
Ok(()) Ok(())
} }
}
pub fn sort( pub fn sort_by(
vec: &mut [Value], vec: &mut [Value],
sort_columns: Vec<String>, comparators: Vec<Comparator>,
span: Span, span: Span,
insensitive: bool, insensitive: bool,
natural: bool, natural: bool,
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
let val_span = vec.first().map(|v| v.span()).unwrap_or(span); if comparators.is_empty() {
match vec.first() {
Some(Value::Record { val: record, .. }) => {
if sort_columns.is_empty() {
// This uses the same format as the 'requires a column name' error in split_by.rs // This uses the same format as the 'requires a column name' error in split_by.rs
return Err(ShellError::GenericError { return Err(ShellError::GenericError {
error: "expected name".into(), error: "expected name".into(),
msg: "requires a column name to sort table data".into(), msg: "requires a cell path or closure to sort data".into(),
span: Some(span), span: Some(span),
help: None, help: None,
inner: vec![], inner: vec![],
}); });
} }
if let Some(nonexistent) = nonexistent_column(&sort_columns, record.columns()) { // to apply insensitive or natural sorting, all values must be strings
return Err(ShellError::CantFindColumn { let string_sort: bool = comparators.iter().all(|cmp| {
col_name: nonexistent, let Comparator::CellPath(cell_path) = cmp else {
span: Some(span), // closures shouldn't affect whether cell paths are sorted naturally/insensitively
src_span: val_span, return true;
};
vec.iter().all(|value| {
let inner = value.clone().follow_cell_path(&cell_path.members, false);
matches!(inner, Ok(Value::String { .. }))
})
}); });
}
// check to make sure each value in each column in the record // allow the comparator function to indicate error
// that we asked for is a string. So, first collect all the columns // by mutating this option captured by the closure,
// that we asked for into vals, then later make sure they're all // since sort_by closure must be infallible
// strings. let mut compare_err: Option<ShellError> = None;
let mut vals = vec![];
for item in vec.iter() {
for col in &sort_columns {
let val = item
.get_data_by_key(col)
.unwrap_or_else(|| Value::nothing(Span::unknown()));
vals.push(val);
}
}
let should_sort_case_insensitively = insensitive
&& vals
.iter()
.all(|x| matches!(x.get_type(), nu_protocol::Type::String));
let should_sort_case_naturally = natural
&& vals
.iter()
.all(|x| matches!(x.get_type(), nu_protocol::Type::String));
vec.sort_by(|a, b| { vec.sort_by(|a, b| {
compare( compare_by(
a, a,
b, b,
&sort_columns, &comparators,
span, span,
should_sort_case_insensitively, insensitive && string_sort,
should_sort_case_naturally, natural && string_sort,
&mut compare_err,
) )
}); });
}
_ => {
vec.sort_by(|a, b| {
if insensitive {
let span_a = a.span();
let span_b = b.span();
let folded_left = match a {
Value::String { val, .. } => Value::string(val.to_folded_case(), span_a),
_ => a.clone(),
};
let folded_right = match b { if let Some(err) = compare_err {
Value::String { val, .. } => Value::string(val.to_folded_case(), span_b), Err(err)
_ => 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 { } 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(()) Ok(())
} }
}
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,
columns: &[String], comparators: &[Comparator],
span: Span, span: Span,
insensitive: bool, insensitive: bool,
natural: bool, natural: bool,
error: &mut Option<ShellError>,
) -> Ordering { ) -> Ordering {
for column in columns { for cmp in comparators.iter() {
let left_value = left.get_data_by_key(column); let result = match cmp {
Comparator::CellPath(cell_path) => {
let left_res = match left_value { compare_cell_path(left, right, cell_path, insensitive, natural)
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 { Comparator::Closure(closure, engine_state, stack) => {
folded_left let closure_eval = ClosureEval::new(engine_state, stack, closure.clone());
.partial_cmp(&folded_right) compare_closure(left, right, closure_eval, span)
.unwrap_or(Ordering::Equal)
} }
} else if natural {
match (
left_res.coerce_into_string(),
right_res.coerce_into_string(),
) {
(Ok(left), Ok(right)) => compare_str(left, right),
_ => Ordering::Equal,
}
} else {
left_res.partial_cmp(&right_res).unwrap_or(Ordering::Equal)
}; };
if result != Ordering::Equal { match result {
return result; Ok(Ordering::Equal) => {}
Ok(ordering) => return ordering,
Err(err) => {
// don't bother continuing through the remaining comparators as we've hit an error
// don't overwrite if there's an existing error
error.get_or_insert(err);
return Ordering::Equal;
}
} }
} }
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(
left: &Value,
right: &Value,
cell_path: &CellPath,
insensitive: bool,
natural: bool,
) -> Result<Ordering, ShellError> {
let left = left.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)
}
pub fn compare_closure(
left: &Value,
right: &Value,
mut closure_eval: ClosureEval,
span: Span,
) -> Result<Ordering, ShellError> {
closure_eval
.add_arg(left.clone())
.add_arg(right.clone())
.run_with_input(PipelineData::Empty)
.and_then(|data| data.into_value(span))
.map(|val| {
if val.is_true() {
Ordering::Less
} else {
Ordering::Greater
}
})
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use nu_protocol::{record, Value}; use nu_protocol::Value;
#[test] #[test]
fn test_sort_value() { fn test_sort_basic() {
let val = Value::test_list(vec![ let mut list = vec![
Value::test_record(record! { Value::test_string("foo"),
"fruit" => Value::test_string("pear"), Value::test_int(2),
"count" => Value::test_int(3), Value::test_int(3),
}), Value::test_string("bar"),
Value::test_record(record! { Value::test_int(1),
"fruit" => Value::test_string("orange"), Value::test_string("baz"),
"count" => Value::test_int(7), ];
}),
Value::test_record(record! {
"fruit" => Value::test_string("apple"),
"count" => Value::test_int(9),
}),
]);
let sorted_alphabetically = assert!(sort(&mut list, false, false).is_ok());
sort_value(&val, vec!["fruit".to_string()], true, false, false).unwrap();
assert_eq!( assert_eq!(
sorted_alphabetically, list,
Value::test_list(vec![ vec![
Value::test_record(record! { Value::test_int(1),
"fruit" => Value::test_string("apple"), Value::test_int(2),
"count" => Value::test_int(9), Value::test_int(3),
}), Value::test_string("bar"),
Value::test_record(record! { Value::test_string("baz"),
"fruit" => Value::test_string("orange"), Value::test_string("foo")
"count" => Value::test_int(7), ]
}),
Value::test_record(record! {
"fruit" => Value::test_string("pear"),
"count" => Value::test_int(3),
}),
],)
);
let sorted_by_count_desc =
sort_value(&val, vec!["count".to_string()], false, false, false).unwrap();
assert_eq!(
sorted_by_count_desc,
Value::test_list(vec![
Value::test_record(record! {
"fruit" => Value::test_string("apple"),
"count" => Value::test_int(9),
}),
Value::test_record(record! {
"fruit" => Value::test_string("orange"),
"count" => Value::test_int(7),
}),
Value::test_record(record! {
"fruit" => Value::test_string("pear"),
"count" => Value::test_int(3),
}),
],)
); );
} }
#[test] #[test]
fn test_sort_value_in_place() { fn test_sort_nothing() {
let mut val = Value::test_list(vec![ // Nothing values should always be sorted to the end of any list
Value::test_record(record! { let mut list = vec![
"fruit" => Value::test_string("pear"), Value::test_int(1),
"count" => Value::test_int(3), Value::test_nothing(),
}), Value::test_int(2),
Value::test_record(record! { Value::test_string("foo"),
"fruit" => Value::test_string("orange"), Value::test_nothing(),
"count" => Value::test_int(7), Value::test_string("bar"),
}), ];
Value::test_record(record! {
"fruit" => Value::test_string("apple"),
"count" => Value::test_int(9),
}),
]);
sort_value_in_place(&mut val, vec!["fruit".to_string()], true, false, false).unwrap(); assert!(sort(&mut list, false, false).is_ok());
assert_eq!( assert_eq!(
val, list,
Value::test_list(vec![ vec![
Value::test_record(record! { Value::test_int(1),
"fruit" => Value::test_string("apple"), Value::test_int(2),
"count" => Value::test_int(9), Value::test_string("bar"),
}), Value::test_string("foo"),
Value::test_record(record! { Value::test_nothing(),
"fruit" => Value::test_string("orange"), Value::test_nothing()
"count" => Value::test_int(7), ]
}),
Value::test_record(record! {
"fruit" => Value::test_string("pear"),
"count" => Value::test_int(3),
}),
],)
); );
sort_value_in_place(&mut val, vec!["count".to_string()], false, false, false).unwrap(); // Ensure that nothing values are sorted after *all* types,
// even types which may follow `Nothing` in the PartialOrd order
// unstable_name_collision
// can be switched to std intersperse when stabilized
let mut values: Vec<_> =
itertools::intersperse(Value::test_values().into_iter(), Value::test_nothing())
.collect();
let nulls = values
.iter()
.filter(|item| item == &&Value::test_nothing())
.count();
assert!(sort(&mut values, false, false).is_ok());
// check if the last `nulls` values of the sorted list are indeed null
assert_eq!(&values[..nulls], vec![Value::test_nothing(); nulls])
}
#[test]
fn test_sort_natural_basic() {
let mut list = vec![
Value::test_string("99"),
Value::test_string("9"),
Value::test_string("1"),
Value::test_string("100"),
Value::test_string("10"),
];
assert!(sort(&mut list, false, false).is_ok());
assert_eq!( assert_eq!(
val, list,
Value::test_list(vec![ vec![
Value::test_record(record! { Value::test_string("1"),
"fruit" => Value::test_string("apple"), Value::test_string("10"),
"count" => Value::test_int(9), Value::test_string("100"),
}), Value::test_string("9"),
Value::test_record(record! { Value::test_string("99"),
"fruit" => Value::test_string("orange"), ]
"count" => Value::test_int(7), );
}),
Value::test_record(record! { assert!(sort(&mut list, false, true).is_ok());
"fruit" => Value::test_string("pear"), assert_eq!(
"count" => Value::test_int(3), list,
}), vec![
],) Value::test_string("1"),
Value::test_string("9"),
Value::test_string("10"),
Value::test_string("99"),
Value::test_string("100"),
]
);
}
#[test]
fn test_sort_natural_mixed_types() {
let mut list = vec![
Value::test_string("1"),
Value::test_int(99),
Value::test_int(1),
Value::test_int(9),
Value::test_string("9"),
Value::test_int(100),
Value::test_string("99"),
Value::test_string("100"),
Value::test_int(10),
Value::test_string("10"),
];
assert!(sort(&mut list, false, false).is_ok());
assert_eq!(
list,
vec![
Value::test_int(1),
Value::test_int(9),
Value::test_int(10),
Value::test_int(99),
Value::test_int(100),
Value::test_string("1"),
Value::test_string("10"),
Value::test_string("100"),
Value::test_string("9"),
Value::test_string("99")
]
);
assert!(sort(&mut list, false, true).is_ok());
assert_eq!(
list,
vec![
Value::test_int(1),
Value::test_string("1"),
Value::test_int(9),
Value::test_string("9"),
Value::test_int(10),
Value::test_string("10"),
Value::test_int(99),
Value::test_string("99"),
Value::test_int(100),
Value::test_string("100"),
]
);
}
#[test]
fn test_sort_natural_no_numeric_values() {
// If list contains no numeric values (numeric strings, ints, floats),
// it should be sorted the same with or without natural sorting
let mut normal = vec![
Value::test_string("golf"),
Value::test_bool(false),
Value::test_string("alfa"),
Value::test_string("echo"),
Value::test_int(7),
Value::test_int(10),
Value::test_bool(true),
Value::test_string("uniform"),
Value::test_int(3),
Value::test_string("tango"),
];
let mut natural = normal.clone();
assert!(sort(&mut normal, false, false).is_ok());
assert!(sort(&mut natural, false, true).is_ok());
assert_eq!(normal, natural);
}
#[test]
fn test_sort_natural_type_order() {
// This test is to prevent regression to a previous natural sort behavior
// where values of different types would be intermixed.
// Only numeric values (ints, floats, and numeric strings) should be intermixed
//
// This list would previously be incorrectly sorted like this:
// ╭────┬─────────╮
// │ 0 │ 1 │
// │ 1 │ golf │
// │ 2 │ false │
// │ 3 │ 7 │
// │ 4 │ 10 │
// │ 5 │ alfa │
// │ 6 │ true │
// │ 7 │ uniform │
// │ 8 │ true │
// │ 9 │ 3 │
// │ 10 │ false │
// │ 11 │ tango │
// ╰────┴─────────╯
let mut list = vec![
Value::test_string("golf"),
Value::test_int(1),
Value::test_bool(false),
Value::test_string("alfa"),
Value::test_int(7),
Value::test_int(10),
Value::test_bool(true),
Value::test_string("uniform"),
Value::test_bool(true),
Value::test_int(3),
Value::test_bool(false),
Value::test_string("tango"),
];
assert!(sort(&mut list, false, true).is_ok());
assert_eq!(
list,
vec![
Value::test_int(1),
Value::test_int(3),
Value::test_int(7),
Value::test_int(10),
Value::test_bool(false),
Value::test_bool(false),
Value::test_bool(true),
Value::test_bool(true),
Value::test_string("alfa"),
Value::test_string("golf"),
Value::test_string("tango"),
Value::test_string("uniform")
]
);
// Only ints, floats, and numeric strings should be intermixed
// While binary primitives and datetimes can be coerced into strings, it doesn't make sense to sort them with numbers
// Binary primitives can hold multiple values, not just one, so shouldn't be compared to single values
// Datetimes don't have a single obvious numeric representation, and if we chose one it would be ambigious to the user
let year_three = chrono::NaiveDate::from_ymd_opt(3, 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
.and_utc();
let mut list = vec![
Value::test_int(10),
Value::test_float(6.0),
Value::test_int(1),
Value::test_binary([3]),
Value::test_string("2"),
Value::test_date(year_three.into()),
Value::test_int(4),
Value::test_binary([52]),
Value::test_float(9.0),
Value::test_string("5"),
Value::test_date(chrono::DateTime::UNIX_EPOCH.into()),
Value::test_int(7),
Value::test_string("8"),
Value::test_float(3.0),
];
assert!(sort(&mut list, false, true).is_ok());
assert_eq!(
list,
vec![
Value::test_int(1),
Value::test_string("2"),
Value::test_float(3.0),
Value::test_int(4),
Value::test_string("5"),
Value::test_float(6.0),
Value::test_int(7),
Value::test_string("8"),
Value::test_float(9.0),
Value::test_int(10),
// the ordering of date and binary here may change if the PartialOrd order is changed,
// but they should not be intermixed with the above
Value::test_binary([3]),
Value::test_binary([52]),
Value::test_date(year_three.into()),
Value::test_date(chrono::DateTime::UNIX_EPOCH.into()),
]
); );
} }
} }