use std::cmp::Ordering;
use super::Value;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Order {
#[default]
Ascending,
Descending,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum Compare {
#[default]
Natural,
Lexical,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SortKey {
pub column: usize,
pub order: Order,
pub compare: Compare,
}
impl SortKey {
pub fn asc(column: usize) -> Self {
SortKey {
column,
order: Order::Ascending,
compare: Compare::Natural,
}
}
pub fn desc(column: usize) -> Self {
SortKey {
order: Order::Descending,
..SortKey::asc(column)
}
}
pub fn lexical(mut self) -> Self {
self.compare = Compare::Lexical;
self
}
}
pub fn indicator(order: Order, ascii: bool) -> &'static str {
match (order, ascii) {
(Order::Ascending, false) => "▲",
(Order::Descending, false) => "▼",
(Order::Ascending, true) => "^",
(Order::Descending, true) => "v",
}
}
fn numeric_str(s: &str) -> Option<f64> {
let s = s.trim();
let digits = s.trim_start_matches(['-', '+']).trim_start_matches('.');
if !digits.starts_with(|c: char| c.is_ascii_digit()) {
return None;
}
s.parse::<f64>().ok()
}
fn numeric(value: &Value) -> Option<f64> {
match value {
Value::Int(n) => Some(*n as f64),
Value::Float(f) => Some(*f),
Value::Str(s) => numeric_str(s),
Value::Text(t) => numeric_str(t.plain()),
Value::Null => None,
}
}
pub fn compare_values(a: &Value, b: &Value, compare: Compare) -> Ordering {
match (a.is_empty(), b.is_empty()) {
(true, true) => return Ordering::Equal,
(true, false) => return Ordering::Greater,
(false, true) => return Ordering::Less,
(false, false) => {}
}
match compare {
Compare::Lexical => a.plain().cmp(&b.plain()),
Compare::Natural => {
if let (Value::Int(x), Value::Int(y)) = (a, b) {
return x.cmp(y);
}
match (numeric(a), numeric(b)) {
(Some(x), Some(y)) => x.total_cmp(&y),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => natural_cmp(&a.plain(), &b.plain()),
}
}
}
}
pub fn natural_cmp(a: &str, b: &str) -> Ordering {
let (mut x, mut y) = (a, b);
loop {
match (x.chars().next(), y.chars().next()) {
(None, None) => return a.cmp(b),
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
(Some(c), Some(d)) if c.is_ascii_digit() && d.is_ascii_digit() => {
let (run_x, rest_x) = split_digits(x);
let (run_y, rest_y) = split_digits(y);
let (value_x, value_y) =
(run_x.trim_start_matches('0'), run_y.trim_start_matches('0'));
let order = value_x
.len()
.cmp(&value_y.len())
.then_with(|| value_x.cmp(value_y));
if order != Ordering::Equal {
return order;
}
(x, y) = (rest_x, rest_y);
}
(Some(c), Some(d)) => {
let order = c.to_lowercase().cmp(d.to_lowercase());
if order != Ordering::Equal {
return order;
}
(x, y) = (&x[c.len_utf8()..], &y[d.len_utf8()..]);
}
}
}
}
fn split_digits(s: &str) -> (&str, &str) {
let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
s.split_at(end)
}
pub fn compare_rows(a: &[Value], b: &[Value], keys: &[SortKey]) -> Ordering {
const NULL: Value = Value::Null;
for key in keys {
let x = a.get(key.column).unwrap_or(&NULL);
let y = b.get(key.column).unwrap_or(&NULL);
let order = match (x.is_empty(), y.is_empty()) {
(false, false) => {
let order = compare_values(x, y, key.compare);
match key.order {
Order::Ascending => order,
Order::Descending => order.reverse(),
}
}
_ => compare_values(x, y, key.compare),
};
if order != Ordering::Equal {
return order;
}
}
Ordering::Equal
}
pub fn sort_rows<R: AsRef<[Value]>>(rows: &mut [R], keys: &[SortKey]) {
if keys.is_empty() {
return;
}
rows.sort_by(|a, b| compare_rows(a.as_ref(), b.as_ref(), keys));
}
pub fn sorted_indices<R: AsRef<[Value]>>(rows: &[R], keys: &[SortKey]) -> Vec<usize> {
let mut order: Vec<usize> = (0..rows.len()).collect();
if !keys.is_empty() {
order.sort_by(|&a, &b| compare_rows(rows[a].as_ref(), rows[b].as_ref(), keys));
}
order
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn natural_order_handles_digits_case_and_leading_zeros() {
let mut words = vec!["a10", "A2", "a2", "a02", "b", "a1b", ""];
words.sort_by(|a, b| natural_cmp(a, b));
assert_eq!(words, ["", "a1b", "A2", "a02", "a2", "a10", "b"]);
}
#[test]
fn numbers_sort_before_text_and_numeric_strings_as_numbers() {
let mut values = [
Value::from("x"),
Value::from("10"),
Value::Float(2.5),
Value::Int(-3),
Value::from("-4.5"),
Value::Null,
];
values.sort_by(|a, b| compare_values(a, b, Compare::Natural));
let plain: Vec<String> = values.iter().map(Value::plain).collect();
assert_eq!(plain, ["-4.5", "-3", "2.5", "10", "x", ""]);
values.sort_by(|a, b| compare_values(a, b, Compare::Lexical));
let plain: Vec<String> = values.iter().map(Value::plain).collect();
assert_eq!(plain, ["-3", "-4.5", "10", "2.5", "x", ""]);
}
}