forked from extern/nushell
Use partial_cmp and make -i case insensitive (#4498)
* Use partial_cmp and make -i case insensitive * Insensitive sort multiple columns
This commit is contained in:
parent
c4e1559f89
commit
5b6156687e
@ -1,10 +1,9 @@
|
|||||||
use chrono::{DateTime, FixedOffset};
|
|
||||||
use nu_engine::{column::column_does_not_exist, CallExt};
|
use nu_engine::{column::column_does_not_exist, CallExt};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::Call,
|
ast::Call,
|
||||||
engine::{Command, EngineState, Stack},
|
engine::{Command, EngineState, Stack},
|
||||||
Category, Config, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature,
|
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span,
|
||||||
Span, SyntaxShape, Value,
|
SyntaxShape, Value,
|
||||||
};
|
};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
@ -112,10 +111,9 @@ impl Command for SortBy {
|
|||||||
let reverse = call.has_flag("reverse");
|
let reverse = call.has_flag("reverse");
|
||||||
let insensitive = call.has_flag("insensitive");
|
let insensitive = call.has_flag("insensitive");
|
||||||
let metadata = &input.metadata();
|
let metadata = &input.metadata();
|
||||||
let config = stack.get_config()?;
|
|
||||||
let mut vec: Vec<_> = input.into_iter().collect();
|
let mut vec: Vec<_> = input.into_iter().collect();
|
||||||
|
|
||||||
sort(&mut vec, columns, call, insensitive, &config)?;
|
sort(&mut vec, columns, call.head, insensitive)?;
|
||||||
|
|
||||||
if reverse {
|
if reverse {
|
||||||
vec.reverse()
|
vec.reverse()
|
||||||
@ -134,9 +132,8 @@ impl Command for SortBy {
|
|||||||
pub fn sort(
|
pub fn sort(
|
||||||
vec: &mut [Value],
|
vec: &mut [Value],
|
||||||
columns: Vec<String>,
|
columns: Vec<String>,
|
||||||
call: &Call,
|
span: Span,
|
||||||
insensitive: bool,
|
insensitive: bool,
|
||||||
config: &Config,
|
|
||||||
) -> Result<(), ShellError> {
|
) -> Result<(), ShellError> {
|
||||||
if vec.is_empty() {
|
if vec.is_empty() {
|
||||||
return Err(ShellError::LabeledError(
|
return Err(ShellError::LabeledError(
|
||||||
@ -153,11 +150,11 @@ pub fn sort(
|
|||||||
} => {
|
} => {
|
||||||
if columns.is_empty() {
|
if columns.is_empty() {
|
||||||
println!("sort-by requires a column name to sort table data");
|
println!("sort-by requires a column name to sort table data");
|
||||||
return Err(ShellError::CantFindColumn(call.head, call.head));
|
return Err(ShellError::CantFindColumn(span, span));
|
||||||
}
|
}
|
||||||
|
|
||||||
if column_does_not_exist(columns.clone(), cols.to_vec()) {
|
if column_does_not_exist(columns.clone(), cols.to_vec()) {
|
||||||
return Err(ShellError::CantFindColumn(call.head, call.head));
|
return Err(ShellError::CantFindColumn(span, span));
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to make sure each value in each column in the record
|
// check to make sure each value in each column in the record
|
||||||
@ -180,33 +177,32 @@ pub fn sort(
|
|||||||
.iter()
|
.iter()
|
||||||
.all(|x| matches!(x.get_type(), nu_protocol::Type::String));
|
.all(|x| matches!(x.get_type(), nu_protocol::Type::String));
|
||||||
|
|
||||||
vec.sort_by(|a, b| {
|
vec.sort_by(|a, b| process(a, b, &columns, span, should_sort_case_insensitively));
|
||||||
process(
|
|
||||||
a,
|
|
||||||
b,
|
|
||||||
&columns[0],
|
|
||||||
call,
|
|
||||||
should_sort_case_insensitively,
|
|
||||||
config,
|
|
||||||
)
|
|
||||||
.expect("sort_by Value::Record bug")
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
vec.sort_by(|a, b| {
|
vec.sort_by(|a, b| {
|
||||||
if insensitive {
|
if insensitive {
|
||||||
let lowercase_left = Value::string(
|
let lowercase_left = match a {
|
||||||
a.into_string("", config).to_ascii_lowercase(),
|
Value::String { val, span } => Value::String {
|
||||||
Span::test_data(),
|
val: val.to_ascii_lowercase(),
|
||||||
);
|
span: *span,
|
||||||
let lowercase_right = Value::string(
|
},
|
||||||
b.into_string("", config).to_ascii_lowercase(),
|
_ => a.clone(),
|
||||||
Span::test_data(),
|
};
|
||||||
);
|
|
||||||
coerce_compare(&lowercase_left, &lowercase_right, call)
|
let lowercase_right = match b {
|
||||||
.expect("sort_by default bug")
|
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 {
|
} else {
|
||||||
coerce_compare(a, b, call).expect("sort_by default bug")
|
a.partial_cmp(b).unwrap_or(Ordering::Equal)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -217,150 +213,53 @@ pub fn sort(
|
|||||||
pub fn process(
|
pub fn process(
|
||||||
left: &Value,
|
left: &Value,
|
||||||
right: &Value,
|
right: &Value,
|
||||||
column: &str,
|
columns: &[String],
|
||||||
call: &Call,
|
span: Span,
|
||||||
insensitive: bool,
|
insensitive: bool,
|
||||||
config: &Config,
|
) -> Ordering {
|
||||||
) -> Result<Ordering, ShellError> {
|
for column in columns {
|
||||||
let left_value = left.get_data_by_key(column);
|
let left_value = left.get_data_by_key(column);
|
||||||
|
|
||||||
let left_res = match left_value {
|
let left_res = match left_value {
|
||||||
Some(left_res) => left_res,
|
Some(left_res) => left_res,
|
||||||
None => Value::Nothing { span: call.head },
|
None => Value::Nothing { span },
|
||||||
};
|
};
|
||||||
|
|
||||||
let right_value = right.get_data_by_key(column);
|
let right_value = right.get_data_by_key(column);
|
||||||
|
|
||||||
let right_res = match right_value {
|
let right_res = match right_value {
|
||||||
Some(right_res) => right_res,
|
Some(right_res) => right_res,
|
||||||
None => Value::Nothing { span: call.head },
|
None => Value::Nothing { span },
|
||||||
};
|
};
|
||||||
|
|
||||||
if insensitive {
|
let result = if insensitive {
|
||||||
let lowercase_left = Value::string(
|
let lowercase_left = match left_res {
|
||||||
left_res.into_string("", config).to_ascii_lowercase(),
|
Value::String { val, span } => Value::String {
|
||||||
Span::test_data(),
|
val: val.to_ascii_lowercase(),
|
||||||
);
|
span,
|
||||||
let lowercase_right = Value::string(
|
},
|
||||||
right_res.into_string("", config).to_ascii_lowercase(),
|
_ => left_res,
|
||||||
Span::test_data(),
|
};
|
||||||
);
|
|
||||||
coerce_compare(&lowercase_left, &lowercase_right, call)
|
|
||||||
} else {
|
|
||||||
coerce_compare(&left_res, &right_res, call)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
let lowercase_right = match right_res {
|
||||||
pub enum CompareValues {
|
Value::String { val, span } => Value::String {
|
||||||
Ints(i64, i64),
|
val: val.to_ascii_lowercase(),
|
||||||
Floats(f64, f64),
|
span,
|
||||||
String(String, String),
|
},
|
||||||
Booleans(bool, bool),
|
_ => right_res,
|
||||||
Filesize(i64, i64),
|
};
|
||||||
Date(DateTime<FixedOffset>, DateTime<FixedOffset>),
|
lowercase_left
|
||||||
}
|
.partial_cmp(&lowercase_right)
|
||||||
|
.unwrap_or(Ordering::Equal)
|
||||||
impl CompareValues {
|
} else {
|
||||||
pub fn compare(&self) -> std::cmp::Ordering {
|
left_res.partial_cmp(&right_res).unwrap_or(Ordering::Equal)
|
||||||
match self {
|
};
|
||||||
CompareValues::Ints(left, right) => left.cmp(right),
|
if result != Ordering::Equal {
|
||||||
CompareValues::Floats(left, right) => process_floats(left, right),
|
return result;
|
||||||
CompareValues::String(left, right) => left.cmp(right),
|
|
||||||
CompareValues::Booleans(left, right) => left.cmp(right),
|
|
||||||
CompareValues::Filesize(left, right) => left.cmp(right),
|
|
||||||
CompareValues::Date(left, right) => left.cmp(right),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process_floats(left: &f64, right: &f64) -> std::cmp::Ordering {
|
Ordering::Equal
|
||||||
let result = left.partial_cmp(right);
|
|
||||||
match result {
|
|
||||||
Some(Ordering::Greater) => Ordering::Greater,
|
|
||||||
Some(Ordering::Less) => Ordering::Less,
|
|
||||||
_ => Ordering::Equal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Arbitrary Order of Values:
|
|
||||||
Floats
|
|
||||||
Ints
|
|
||||||
Strings
|
|
||||||
Bools
|
|
||||||
Lists
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn coerce_compare(left: &Value, right: &Value, call: &Call) -> Result<Ordering, ShellError> {
|
|
||||||
Ok(match (left, right) {
|
|
||||||
(Value::Float { val: left, .. }, Value::Float { val: right, .. }) => {
|
|
||||||
CompareValues::Floats(*left, *right).compare()
|
|
||||||
}
|
|
||||||
(Value::Filesize { val: left, .. }, Value::Filesize { val: right, .. }) => {
|
|
||||||
CompareValues::Filesize(*left, *right).compare()
|
|
||||||
}
|
|
||||||
(Value::Date { val: left, .. }, Value::Date { val: right, .. }) => {
|
|
||||||
CompareValues::Date(*left, *right).compare()
|
|
||||||
}
|
|
||||||
(Value::Int { val: left, .. }, Value::Int { val: right, .. }) => {
|
|
||||||
CompareValues::Ints(*left, *right).compare()
|
|
||||||
}
|
|
||||||
(Value::String { val: left, .. }, Value::String { val: right, .. }) => {
|
|
||||||
CompareValues::String(left.clone(), right.clone()).compare()
|
|
||||||
}
|
|
||||||
(Value::Bool { val: left, .. }, Value::Bool { val: right, .. }) => {
|
|
||||||
CompareValues::Booleans(*left, *right).compare()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Not sure how to compare and sort lists
|
|
||||||
(Value::List { .. }, Value::List { .. }) => Ordering::Equal,
|
|
||||||
|
|
||||||
// Floats will always come before Ints
|
|
||||||
(Value::Float { .. }, Value::Int { .. }) => Ordering::Less,
|
|
||||||
(Value::Int { .. }, Value::Float { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Floats will always come before Strings
|
|
||||||
(Value::Float { .. }, Value::String { .. }) => Ordering::Less,
|
|
||||||
(Value::String { .. }, Value::Float { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Floats will always come before Bools
|
|
||||||
(Value::Float { .. }, Value::Bool { .. }) => Ordering::Less,
|
|
||||||
(Value::Bool { .. }, Value::Float { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Floats will always come before Lists
|
|
||||||
(Value::Float { .. }, Value::List { .. }) => Ordering::Less,
|
|
||||||
(Value::List { .. }, Value::Float { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Ints will always come before strings
|
|
||||||
(Value::Int { .. }, Value::String { .. }) => Ordering::Less,
|
|
||||||
(Value::String { .. }, Value::Int { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Ints will always come before Bools
|
|
||||||
(Value::Int { .. }, Value::Bool { .. }) => Ordering::Less,
|
|
||||||
(Value::Bool { .. }, Value::Int { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Ints will always come before Lists
|
|
||||||
(Value::Int { .. }, Value::List { .. }) => Ordering::Less,
|
|
||||||
(Value::List { .. }, Value::Int { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Strings will always come before Bools
|
|
||||||
(Value::String { .. }, Value::Bool { .. }) => Ordering::Less,
|
|
||||||
(Value::Bool { .. }, Value::String { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Strings will always come before Lists
|
|
||||||
(Value::String { .. }, Value::List { .. }) => Ordering::Less,
|
|
||||||
(Value::List { .. }, Value::String { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
// Bools will always come before Lists
|
|
||||||
(Value::Bool { .. }, Value::List { .. }) => Ordering::Less,
|
|
||||||
(Value::List { .. }, Value::Bool { .. }) => Ordering::Greater,
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
let description = format!("not able to compare {:?} with {:?}\n", left, right);
|
|
||||||
return Err(ShellError::TypeMismatch(description, call.head));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -127,7 +127,7 @@ fn ls_sort_by_type_name_sensitive() {
|
|||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
let json_output = r#"[{"name": "C","type": "Dir"},{"name": "a.txt","type": "File"},{"name": "B.txt","type": "File"}]"#;
|
let json_output = r#"[{"name": "C","type": "Dir"},{"name": "B.txt","type": "File"},{"name": "a.txt","type": "File"}]"#;
|
||||||
assert_eq!(actual.out, json_output);
|
assert_eq!(actual.out, json_output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,3 +89,19 @@ fn single_tick_interpolation() -> TestResult {
|
|||||||
fn detect_newlines() -> TestResult {
|
fn detect_newlines() -> TestResult {
|
||||||
run_test("'hello\r\nworld' | lines | get 0 | str length", "5")
|
run_test("'hello\r\nworld' | lines | get 0 | str length", "5")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn case_insensitive_sort() -> TestResult {
|
||||||
|
run_test(
|
||||||
|
r#"[a, B, d, C, f] | sort-by -i | to json --raw"#,
|
||||||
|
"[\"a\",\"B\",\"C\",\"d\",\"f\"]",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn case_insensitive_sort_columns() -> TestResult {
|
||||||
|
run_test(
|
||||||
|
r#"[[version, package]; ["two", "Abc"], ["three", "abc"], ["four", "abc"]] | sort-by -i package version | to json --raw"#,
|
||||||
|
r#"[{"version": "four","package": "abc"},{"version": "three","package": "abc"},{"version": "two","package": "Abc"}]"#,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user