Make alphabetical sorting the default (#32315)
Follow up of this pr: #25148 Release Notes: - Improved file sorting. As described in #20126, I was fed up with lexicographical file sorting in the project panel. The current sorting behavior doesn't handle numeric segments properly, leading to unintuitive ordering like `file_1.rs`, `file_10.rs`, `file_2.rs`. ## Example Sorting Results Using `lexicographical` (default): ``` . ├── file_01.rs ├── file_1.rs ├── file_10.rs ├── file_1025.rs ├── file_2.rs ``` Using alphabetical (natural) sorting: ``` . ├── file_1.rs ├── file_01.rs ├── file_2.rs ├── file_10.rs ├── file_1025.rs ```
This commit is contained in:
parent
293992f5b1
commit
e67b2da20c
2 changed files with 520 additions and 35 deletions
|
@ -740,9 +740,9 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
|||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > [EDITOR: ''] <== selected",
|
||||
" > 3",
|
||||
" > 4",
|
||||
" > [EDITOR: ''] <== selected",
|
||||
" a-different-filename.tar.gz",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
|
@ -765,10 +765,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
|||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" > 4",
|
||||
" > [PROCESSING: 'new-dir']",
|
||||
" a-different-filename.tar.gz <== selected",
|
||||
" > 3 <== selected",
|
||||
" > 4",
|
||||
" a-different-filename.tar.gz",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
]
|
||||
|
@ -782,10 +782,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
|||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" > 3 <== selected",
|
||||
" > 4",
|
||||
" > new-dir",
|
||||
" a-different-filename.tar.gz <== selected",
|
||||
" a-different-filename.tar.gz",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
]
|
||||
|
@ -801,10 +801,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
|||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" > [EDITOR: '3'] <== selected",
|
||||
" > 4",
|
||||
" > new-dir",
|
||||
" [EDITOR: 'a-different-filename.tar.gz'] <== selected",
|
||||
" a-different-filename.tar.gz",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
]
|
||||
|
@ -819,10 +819,10 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
|||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" > 3 <== selected",
|
||||
" > 4",
|
||||
" > new-dir",
|
||||
" a-different-filename.tar.gz <== selected",
|
||||
" a-different-filename.tar.gz",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
]
|
||||
|
@ -837,12 +837,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
|||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" v 3",
|
||||
" [EDITOR: ''] <== selected",
|
||||
" Q",
|
||||
" > 4",
|
||||
" > new-dir",
|
||||
" [EDITOR: ''] <== selected",
|
||||
" a-different-filename.tar.gz",
|
||||
" > C",
|
||||
]
|
||||
);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
|
@ -863,12 +863,12 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
|||
" > .git",
|
||||
" > a",
|
||||
" v b",
|
||||
" > 3",
|
||||
" v 3 <== selected",
|
||||
" Q",
|
||||
" > 4",
|
||||
" > new-dir",
|
||||
" a-different-filename.tar.gz <== selected",
|
||||
" a-different-filename.tar.gz",
|
||||
" > C",
|
||||
" .dockerignore",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use std::cmp;
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::path::StripPrefixError;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::{
|
||||
|
@ -7,12 +10,6 @@ use std::{
|
|||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::NumericPrefixWithSuffix;
|
||||
|
||||
/// Returns the path to the user's home directory.
|
||||
pub fn home_dir() -> &'static PathBuf {
|
||||
static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
@ -545,17 +542,172 @@ impl PathMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
/// Custom character comparison that prioritizes lowercase for same letters
|
||||
fn compare_chars(a: char, b: char) -> Ordering {
|
||||
// First compare case-insensitive
|
||||
match a.to_ascii_lowercase().cmp(&b.to_ascii_lowercase()) {
|
||||
Ordering::Equal => {
|
||||
// If same letter, prioritize lowercase (lowercase < uppercase)
|
||||
match (a.is_ascii_lowercase(), b.is_ascii_lowercase()) {
|
||||
(true, false) => Ordering::Less, // lowercase comes first
|
||||
(false, true) => Ordering::Greater, // uppercase comes after
|
||||
_ => Ordering::Equal, // both same case or both non-ascii
|
||||
}
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two sequences of consecutive digits for natural sorting.
|
||||
///
|
||||
/// This function is a core component of natural sorting that handles numeric comparison
|
||||
/// in a way that feels natural to humans. It extracts and compares consecutive digit
|
||||
/// sequences from two iterators, handling various cases like leading zeros and very large numbers.
|
||||
///
|
||||
/// # Behavior
|
||||
///
|
||||
/// The function implements the following comparison rules:
|
||||
/// 1. Different numeric values: Compares by actual numeric value (e.g., "2" < "10")
|
||||
/// 2. Leading zeros: When values are equal, longer sequence wins (e.g., "002" > "2")
|
||||
/// 3. Large numbers: Falls back to string comparison for numbers that would overflow u128
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```text
|
||||
/// "1" vs "2" -> Less (different values)
|
||||
/// "2" vs "10" -> Less (numeric comparison)
|
||||
/// "002" vs "2" -> Greater (leading zeros)
|
||||
/// "10" vs "010" -> Less (leading zeros)
|
||||
/// "999..." vs "1000..." -> Less (large number comparison)
|
||||
/// ```
|
||||
///
|
||||
/// # Implementation Details
|
||||
///
|
||||
/// 1. Extracts consecutive digits into strings
|
||||
/// 2. Compares sequence lengths for leading zero handling
|
||||
/// 3. For equal lengths, compares digit by digit
|
||||
/// 4. For different lengths:
|
||||
/// - Attempts numeric comparison first (for numbers up to 2^128 - 1)
|
||||
/// - Falls back to string comparison if numbers would overflow
|
||||
///
|
||||
/// The function advances both iterators past their respective numeric sequences,
|
||||
/// regardless of the comparison result.
|
||||
fn compare_numeric_segments<I>(
|
||||
a_iter: &mut std::iter::Peekable<I>,
|
||||
b_iter: &mut std::iter::Peekable<I>,
|
||||
) -> Ordering
|
||||
where
|
||||
I: Iterator<Item = char>,
|
||||
{
|
||||
// Collect all consecutive digits into strings
|
||||
let mut a_num_str = String::new();
|
||||
let mut b_num_str = String::new();
|
||||
|
||||
while let Some(&c) = a_iter.peek() {
|
||||
if !c.is_ascii_digit() {
|
||||
break;
|
||||
}
|
||||
|
||||
a_num_str.push(c);
|
||||
a_iter.next();
|
||||
}
|
||||
|
||||
while let Some(&c) = b_iter.peek() {
|
||||
if !c.is_ascii_digit() {
|
||||
break;
|
||||
}
|
||||
|
||||
b_num_str.push(c);
|
||||
b_iter.next();
|
||||
}
|
||||
|
||||
// First compare lengths (handle leading zeros)
|
||||
match a_num_str.len().cmp(&b_num_str.len()) {
|
||||
Ordering::Equal => {
|
||||
// Same length, compare digit by digit
|
||||
match a_num_str.cmp(&b_num_str) {
|
||||
Ordering::Equal => Ordering::Equal,
|
||||
ordering => ordering,
|
||||
}
|
||||
}
|
||||
|
||||
// Different lengths but same value means leading zeros
|
||||
ordering => {
|
||||
// Try parsing as numbers first
|
||||
if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::<u128>(), b_num_str.parse::<u128>()) {
|
||||
match a_val.cmp(&b_val) {
|
||||
Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros)
|
||||
ord => ord,
|
||||
}
|
||||
} else {
|
||||
// If parsing fails (overflow), compare as strings
|
||||
a_num_str.cmp(&b_num_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs natural sorting comparison between two strings.
|
||||
///
|
||||
/// Natural sorting is an ordering that handles numeric sequences in a way that matches human expectations.
|
||||
/// For example, "file2" comes before "file10" (unlike standard lexicographic sorting).
|
||||
///
|
||||
/// # Characteristics
|
||||
///
|
||||
/// * Case-sensitive with lowercase priority: When comparing same letters, lowercase comes before uppercase
|
||||
/// * Numbers are compared by numeric value, not character by character
|
||||
/// * Leading zeros affect ordering when numeric values are equal
|
||||
/// * Can handle numbers larger than u128::MAX (falls back to string comparison)
|
||||
///
|
||||
/// # Algorithm
|
||||
///
|
||||
/// The function works by:
|
||||
/// 1. Processing strings character by character
|
||||
/// 2. When encountering digits, treating consecutive digits as a single number
|
||||
/// 3. Comparing numbers by their numeric value rather than lexicographically
|
||||
/// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority
|
||||
fn natural_sort(a: &str, b: &str) -> Ordering {
|
||||
let mut a_iter = a.chars().peekable();
|
||||
let mut b_iter = b.chars().peekable();
|
||||
|
||||
loop {
|
||||
match (a_iter.peek(), b_iter.peek()) {
|
||||
(None, None) => return Ordering::Equal,
|
||||
(None, _) => return Ordering::Less,
|
||||
(_, None) => return Ordering::Greater,
|
||||
(Some(&a_char), Some(&b_char)) => {
|
||||
if a_char.is_ascii_digit() && b_char.is_ascii_digit() {
|
||||
match compare_numeric_segments(&mut a_iter, &mut b_iter) {
|
||||
Ordering::Equal => continue,
|
||||
ordering => return ordering,
|
||||
}
|
||||
} else {
|
||||
match compare_chars(a_char, b_char) {
|
||||
Ordering::Equal => {
|
||||
a_iter.next();
|
||||
b_iter.next();
|
||||
}
|
||||
ordering => return ordering,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_paths(
|
||||
(path_a, a_is_file): (&Path, bool),
|
||||
(path_b, b_is_file): (&Path, bool),
|
||||
) -> cmp::Ordering {
|
||||
) -> Ordering {
|
||||
let mut components_a = path_a.components().peekable();
|
||||
let mut components_b = path_b.components().peekable();
|
||||
|
||||
loop {
|
||||
match (components_a.next(), components_b.next()) {
|
||||
(Some(component_a), Some(component_b)) => {
|
||||
let a_is_file = components_a.peek().is_none() && a_is_file;
|
||||
let b_is_file = components_b.peek().is_none() && b_is_file;
|
||||
|
||||
let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
|
||||
let path_a = Path::new(component_a.as_os_str());
|
||||
let path_string_a = if a_is_file {
|
||||
|
@ -564,9 +716,6 @@ pub fn compare_paths(
|
|||
path_a.file_name()
|
||||
}
|
||||
.map(|s| s.to_string_lossy());
|
||||
let num_and_remainder_a = path_string_a
|
||||
.as_deref()
|
||||
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
|
||||
|
||||
let path_b = Path::new(component_b.as_os_str());
|
||||
let path_string_b = if b_is_file {
|
||||
|
@ -575,27 +724,32 @@ pub fn compare_paths(
|
|||
path_b.file_name()
|
||||
}
|
||||
.map(|s| s.to_string_lossy());
|
||||
let num_and_remainder_b = path_string_b
|
||||
.as_deref()
|
||||
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
|
||||
|
||||
num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| {
|
||||
let compare_components = match (path_string_a, path_string_b) {
|
||||
(Some(a), Some(b)) => natural_sort(&a, &b),
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(None, None) => Ordering::Equal,
|
||||
};
|
||||
|
||||
compare_components.then_with(|| {
|
||||
if a_is_file && b_is_file {
|
||||
let ext_a = path_a.extension().unwrap_or_default();
|
||||
let ext_b = path_b.extension().unwrap_or_default();
|
||||
ext_a.cmp(ext_b)
|
||||
} else {
|
||||
cmp::Ordering::Equal
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if !ordering.is_eq() {
|
||||
return ordering;
|
||||
}
|
||||
}
|
||||
(Some(_), None) => break cmp::Ordering::Greater,
|
||||
(None, Some(_)) => break cmp::Ordering::Less,
|
||||
(None, None) => break cmp::Ordering::Equal,
|
||||
(Some(_), None) => break Ordering::Greater,
|
||||
(None, Some(_)) => break Ordering::Less,
|
||||
(None, None) => break Ordering::Equal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1049,4 +1203,335 @@ mod tests {
|
|||
"C:\\Users\\someone\\test_file.rs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_numeric_segments() {
|
||||
// Helper function to create peekable iterators and test
|
||||
fn compare(a: &str, b: &str) -> Ordering {
|
||||
let mut a_iter = a.chars().peekable();
|
||||
let mut b_iter = b.chars().peekable();
|
||||
|
||||
let result = compare_numeric_segments(&mut a_iter, &mut b_iter);
|
||||
|
||||
// Verify iterators advanced correctly
|
||||
assert!(
|
||||
!a_iter.next().map_or(false, |c| c.is_ascii_digit()),
|
||||
"Iterator a should have consumed all digits"
|
||||
);
|
||||
assert!(
|
||||
!b_iter.next().map_or(false, |c| c.is_ascii_digit()),
|
||||
"Iterator b should have consumed all digits"
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// Basic numeric comparisons
|
||||
assert_eq!(compare("0", "0"), Ordering::Equal);
|
||||
assert_eq!(compare("1", "2"), Ordering::Less);
|
||||
assert_eq!(compare("9", "10"), Ordering::Less);
|
||||
assert_eq!(compare("10", "9"), Ordering::Greater);
|
||||
assert_eq!(compare("99", "100"), Ordering::Less);
|
||||
|
||||
// Leading zeros
|
||||
assert_eq!(compare("0", "00"), Ordering::Less);
|
||||
assert_eq!(compare("00", "0"), Ordering::Greater);
|
||||
assert_eq!(compare("01", "1"), Ordering::Greater);
|
||||
assert_eq!(compare("001", "1"), Ordering::Greater);
|
||||
assert_eq!(compare("001", "01"), Ordering::Greater);
|
||||
|
||||
// Same value different representation
|
||||
assert_eq!(compare("000100", "100"), Ordering::Greater);
|
||||
assert_eq!(compare("100", "0100"), Ordering::Less);
|
||||
assert_eq!(compare("0100", "00100"), Ordering::Less);
|
||||
|
||||
// Large numbers
|
||||
assert_eq!(compare("9999999999", "10000000000"), Ordering::Less);
|
||||
assert_eq!(
|
||||
compare(
|
||||
"340282366920938463463374607431768211455", // u128::MAX
|
||||
"340282366920938463463374607431768211456"
|
||||
),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
compare(
|
||||
"340282366920938463463374607431768211456", // > u128::MAX
|
||||
"340282366920938463463374607431768211455"
|
||||
),
|
||||
Ordering::Greater
|
||||
);
|
||||
|
||||
// Iterator advancement verification
|
||||
let mut a_iter = "123abc".chars().peekable();
|
||||
let mut b_iter = "456def".chars().peekable();
|
||||
|
||||
compare_numeric_segments(&mut a_iter, &mut b_iter);
|
||||
|
||||
assert_eq!(a_iter.collect::<String>(), "abc");
|
||||
assert_eq!(b_iter.collect::<String>(), "def");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_natural_sort() {
|
||||
// Basic alphanumeric
|
||||
assert_eq!(natural_sort("a", "b"), Ordering::Less);
|
||||
assert_eq!(natural_sort("b", "a"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("a", "a"), Ordering::Equal);
|
||||
|
||||
// Case sensitivity
|
||||
assert_eq!(natural_sort("a", "A"), Ordering::Less);
|
||||
assert_eq!(natural_sort("A", "a"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("aA", "aa"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("aa", "aA"), Ordering::Less);
|
||||
|
||||
// Numbers
|
||||
assert_eq!(natural_sort("1", "2"), Ordering::Less);
|
||||
assert_eq!(natural_sort("2", "10"), Ordering::Less);
|
||||
assert_eq!(natural_sort("02", "10"), Ordering::Less);
|
||||
assert_eq!(natural_sort("02", "2"), Ordering::Greater);
|
||||
|
||||
// Mixed alphanumeric
|
||||
assert_eq!(natural_sort("a1", "a2"), Ordering::Less);
|
||||
assert_eq!(natural_sort("a2", "a10"), Ordering::Less);
|
||||
assert_eq!(natural_sort("a02", "a2"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less);
|
||||
|
||||
// Multiple numeric segments
|
||||
assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less);
|
||||
assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less);
|
||||
|
||||
// Special characters
|
||||
assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less);
|
||||
assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less);
|
||||
assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less);
|
||||
|
||||
// Unicode
|
||||
assert_eq!(natural_sort("文1", "文2"), Ordering::Less);
|
||||
assert_eq!(natural_sort("文2", "文10"), Ordering::Less);
|
||||
assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less);
|
||||
|
||||
// Empty and special cases
|
||||
assert_eq!(natural_sort("", ""), Ordering::Equal);
|
||||
assert_eq!(natural_sort("", "a"), Ordering::Less);
|
||||
assert_eq!(natural_sort("a", ""), Ordering::Greater);
|
||||
assert_eq!(natural_sort(" ", " "), Ordering::Less);
|
||||
|
||||
// Mixed everything
|
||||
assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less);
|
||||
assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less);
|
||||
assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less);
|
||||
assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_paths() {
|
||||
// Helper function for cleaner tests
|
||||
fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering {
|
||||
compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file))
|
||||
}
|
||||
|
||||
// Basic path comparison
|
||||
assert_eq!(compare("a", true, "b", true), Ordering::Less);
|
||||
assert_eq!(compare("b", true, "a", true), Ordering::Greater);
|
||||
assert_eq!(compare("a", true, "a", true), Ordering::Equal);
|
||||
|
||||
// Files vs Directories
|
||||
assert_eq!(compare("a", true, "a", false), Ordering::Greater);
|
||||
assert_eq!(compare("a", false, "a", true), Ordering::Less);
|
||||
assert_eq!(compare("b", false, "a", true), Ordering::Less);
|
||||
|
||||
// Extensions
|
||||
assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater);
|
||||
assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less);
|
||||
assert_eq!(compare("a", true, "a.txt", true), Ordering::Less);
|
||||
|
||||
// Nested paths
|
||||
assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less);
|
||||
assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less);
|
||||
assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less);
|
||||
|
||||
// Case sensitivity in paths
|
||||
assert_eq!(
|
||||
compare("Dir/file", true, "dir/file", true),
|
||||
Ordering::Greater
|
||||
);
|
||||
assert_eq!(
|
||||
compare("dir/File", true, "dir/file", true),
|
||||
Ordering::Greater
|
||||
);
|
||||
assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less);
|
||||
|
||||
// Hidden files and special names
|
||||
assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less);
|
||||
assert_eq!(compare("_special", true, "normal", true), Ordering::Less);
|
||||
assert_eq!(compare(".config", false, ".data", false), Ordering::Less);
|
||||
|
||||
// Mixed numeric paths
|
||||
assert_eq!(
|
||||
compare("dir1/file", true, "dir2/file", true),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
compare("dir2/file", true, "dir10/file", true),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
compare("dir02/file", true, "dir2/file", true),
|
||||
Ordering::Greater
|
||||
);
|
||||
|
||||
// Root paths
|
||||
assert_eq!(compare("/a", true, "/b", true), Ordering::Less);
|
||||
assert_eq!(compare("/", false, "/a", true), Ordering::Less);
|
||||
|
||||
// Complex real-world examples
|
||||
assert_eq!(
|
||||
compare("project/src/main.rs", true, "project/src/lib.rs", true),
|
||||
Ordering::Greater
|
||||
);
|
||||
assert_eq!(
|
||||
compare(
|
||||
"project/tests/test_1.rs",
|
||||
true,
|
||||
"project/tests/test_2.rs",
|
||||
true
|
||||
),
|
||||
Ordering::Less
|
||||
);
|
||||
assert_eq!(
|
||||
compare(
|
||||
"project/v1.0.0/README.md",
|
||||
true,
|
||||
"project/v1.10.0/README.md",
|
||||
true
|
||||
),
|
||||
Ordering::Less
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_natural_sort_case_sensitivity() {
|
||||
// Same letter different case - lowercase should come first
|
||||
assert_eq!(natural_sort("a", "A"), Ordering::Less);
|
||||
assert_eq!(natural_sort("A", "a"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("a", "a"), Ordering::Equal);
|
||||
assert_eq!(natural_sort("A", "A"), Ordering::Equal);
|
||||
|
||||
// Mixed case strings
|
||||
assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less);
|
||||
assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less);
|
||||
|
||||
// Different letters
|
||||
assert_eq!(natural_sort("a", "b"), Ordering::Less);
|
||||
assert_eq!(natural_sort("A", "b"), Ordering::Less);
|
||||
assert_eq!(natural_sort("a", "B"), Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_natural_sort_with_numbers() {
|
||||
// Basic number ordering
|
||||
assert_eq!(natural_sort("file1", "file2"), Ordering::Less);
|
||||
assert_eq!(natural_sort("file2", "file10"), Ordering::Less);
|
||||
assert_eq!(natural_sort("file10", "file2"), Ordering::Greater);
|
||||
|
||||
// Numbers in different positions
|
||||
assert_eq!(natural_sort("1file", "2file"), Ordering::Less);
|
||||
assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less);
|
||||
assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less);
|
||||
|
||||
// Multiple numbers in string
|
||||
assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less);
|
||||
assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less);
|
||||
|
||||
// Leading zeros
|
||||
assert_eq!(natural_sort("file002", "file2"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("file002", "file10"), Ordering::Less);
|
||||
|
||||
// Very large numbers
|
||||
assert_eq!(
|
||||
natural_sort("file999999999999999999999", "file999999999999999999998"),
|
||||
Ordering::Greater
|
||||
);
|
||||
|
||||
// u128 edge cases
|
||||
|
||||
// Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455)
|
||||
assert_eq!(
|
||||
natural_sort(
|
||||
"file340282366920938463463374607431768211454",
|
||||
"file340282366920938463463374607431768211455"
|
||||
),
|
||||
Ordering::Less
|
||||
);
|
||||
|
||||
// Equal length numbers that overflow u128
|
||||
assert_eq!(
|
||||
natural_sort(
|
||||
"file340282366920938463463374607431768211456",
|
||||
"file340282366920938463463374607431768211455"
|
||||
),
|
||||
Ordering::Greater
|
||||
);
|
||||
|
||||
// Different length numbers that overflow u128
|
||||
assert_eq!(
|
||||
natural_sort(
|
||||
"file3402823669209384634633746074317682114560",
|
||||
"file340282366920938463463374607431768211455"
|
||||
),
|
||||
Ordering::Greater
|
||||
);
|
||||
|
||||
// Leading zeros with numbers near u128::MAX
|
||||
assert_eq!(
|
||||
natural_sort(
|
||||
"file0340282366920938463463374607431768211455",
|
||||
"file340282366920938463463374607431768211455"
|
||||
),
|
||||
Ordering::Greater
|
||||
);
|
||||
|
||||
// Very large numbers with different lengths (both overflow u128)
|
||||
assert_eq!(
|
||||
natural_sort(
|
||||
"file999999999999999999999999999999999999999999999999",
|
||||
"file9999999999999999999999999999999999999999999999999"
|
||||
),
|
||||
Ordering::Less
|
||||
);
|
||||
|
||||
// Mixed case with numbers
|
||||
assert_eq!(natural_sort("File1", "file2"), Ordering::Greater);
|
||||
assert_eq!(natural_sort("file1", "File2"), Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_natural_sort_edge_cases() {
|
||||
// Empty strings
|
||||
assert_eq!(natural_sort("", ""), Ordering::Equal);
|
||||
assert_eq!(natural_sort("", "a"), Ordering::Less);
|
||||
assert_eq!(natural_sort("a", ""), Ordering::Greater);
|
||||
|
||||
// Special characters
|
||||
assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less);
|
||||
assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less);
|
||||
assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less);
|
||||
|
||||
// Unicode characters
|
||||
// 9312 vs 9313
|
||||
assert_eq!(natural_sort("file①", "file②"), Ordering::Less);
|
||||
// 9321 vs 9313
|
||||
assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater);
|
||||
// 28450 vs 23383
|
||||
assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater);
|
||||
|
||||
// Mixed alphanumeric with special chars
|
||||
assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less);
|
||||
assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less);
|
||||
assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue