1use core::cmp::{Ordering, PartialOrd};
2
3pub fn sort(arr: &mut [&str]) {
26 arr.sort_by(|a, b| compare(a, b));
27}
28
29pub fn compare(a: &str, b: &str) -> Ordering {
53 let mut a = a;
55 let mut b = b;
56
57 if let Some(cmp) = match (a, b) {
61 ("", "") | (".", ".") | ("..", "..") => Some(Ordering::Equal),
62 ("", _) => Some(Ordering::Less),
63 (_, "") => Some(Ordering::Greater),
64 (".", _) => Some(Ordering::Less),
65 (_, ".") => Some(Ordering::Greater),
66 ("..", _) => Some(Ordering::Less),
67 (_, "..") => Some(Ordering::Greater),
68 _ => None,
69 } {
70 return cmp;
71 };
72
73 match (a.starts_with('.'), b.starts_with('.')) {
76 (true, false) => return Ordering::Less,
77 (false, true) => return Ordering::Greater,
78 (false, false) => {}
79 (true, true) => {
80 a = if a.len() == 1 { "" } else { &a[1..] };
81 b = if b.len() == 1 { "" } else { &b[1..] };
82 }
83 }
84
85 let cmp = sequence_cmp(split_extension(a).0, split_extension(b).0);
87 if cmp != Ordering::Equal {
88 return cmp;
89 }
90 let cmp = sequence_cmp(a, b);
92 if cmp != Ordering::Equal {
93 return cmp;
94 }
95 a.cmp(b)
98}
99
100fn sequence_cmp(a: &str, b: &str) -> Ordering {
103 let mut a_str = a;
104 let mut b_str = b;
105 loop {
106 let (a_non_digit_part, remaining_a) = non_digit_seq(a_str);
107 let (b_non_digit_part, remaining_b) = non_digit_seq(b_str);
108 let cmp = compare_non_digit_seq(a_non_digit_part, b_non_digit_part);
109 if cmp != Ordering::Equal {
110 return cmp;
111 }
112 let (a_digit_part, remaining_a) = digit_seq(remaining_a);
113 let (b_digit_part, remaining_b) = digit_seq(remaining_b);
114
115 let a_digits = a_digit_part.parse::<u64>().unwrap_or_default();
117 let b_digits = b_digit_part.parse::<u64>().unwrap_or_default();
118 let cmp = a_digits.cmp(&b_digits);
119 if cmp != Ordering::Equal {
120 return cmp;
121 }
122
123 a_str = remaining_a;
124 b_str = remaining_b;
125
126 if a_str.is_empty() && b_str.is_empty() {
128 return Ordering::Equal;
129 }
130 }
131}
132
133fn split_extension(s: &str) -> (&str, &str) {
149 let mut split_ind: Option<usize> = None;
154 let mut last_char: Option<char> = None;
155 for (i, c) in s.char_indices().rev() {
156 if c == '.' {
158 match last_char {
159 None => return (s, ""),
161 Some(prev_char) => {
162 if prev_char.is_ascii_alphabetic() || prev_char == '~' {
164 split_ind = Some(i);
165 } else {
166 break;
167 }
168 }
169 }
170 } else if !(c.is_ascii_alphanumeric() || c == '~') {
171 break;
172 }
173 last_char = Some(c);
175 }
176
177 split_ind.map_or((s, ""), |ind| s.split_at(ind))
178}
179
180#[derive(Eq)]
181struct VersionSortChar(Option<u8>);
182
183impl From<Option<u8>> for VersionSortChar {
184 fn from(c: Option<u8>) -> Self {
185 Self(c)
186 }
187}
188
189impl PartialOrd for VersionSortChar {
190 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
195 match (self.0, other.0) {
196 (None, None) => Some(Ordering::Equal),
197 (Some(a), None) => {
198 if a == b'~' {
199 Some(Ordering::Less)
200 } else {
201 Some(Ordering::Greater)
202 }
203 }
204 (None, Some(b)) => {
205 if b == b'~' {
206 Some(Ordering::Greater)
207 } else {
208 Some(Ordering::Less)
209 }
210 }
211 (Some(a), Some(b)) => {
212 if a == b {
213 return Some(Ordering::Equal);
214 }
215 if a == b'~' {
216 return Some(Ordering::Less);
217 }
218 if b == b'~' {
219 return Some(Ordering::Greater);
220 }
221 match (a.is_ascii_alphabetic(), b.is_ascii_alphabetic()) {
222 (true, true) | (false, false) => Some(a.cmp(&b)),
225 (true, false) => Some(Ordering::Less),
226 (false, true) => Some(Ordering::Greater),
227 }
228 }
229 }
230 }
231}
232
233impl PartialEq for VersionSortChar {
234 fn eq(&self, other: &Self) -> bool {
235 self.0 == other.0
236 }
237}
238
239fn compare_non_digit_seq(a: &str, b: &str) -> Ordering {
240 let mut a_bytes = a.bytes();
241 let mut b_bytes = b.bytes();
242 loop {
243 let a_byte = a_bytes.next();
244 let b_byte = b_bytes.next();
245 if a_byte.is_none() && b_byte.is_none() {
246 return Ordering::Equal;
247 }
248 let cmp = VersionSortChar::from(a_byte)
249 .partial_cmp(&VersionSortChar::from(b_byte))
250 .unwrap();
251 if cmp == Ordering::Equal {
252 continue;
253 }
254 return cmp;
255 }
256}
257
258fn non_digit_seq(a: &str) -> (&str, &str) {
259 a.bytes()
260 .enumerate()
261 .find(|(_, c)| c.is_ascii_digit())
262 .map_or((a, ""), |(index, _)| a.split_at(index))
263}
264
265fn digit_seq(a: &str) -> (&str, &str) {
266 a.bytes()
267 .enumerate()
268 .find(|(_, c)| !c.is_ascii_digit())
269 .map_or((a, ""), |(index, _)| a.split_at(index))
270}
271
272#[cfg(test)]
273mod test {
274 use test_case::test_case;
275
276 use super::*;
277
278 #[test_case(vec!["", "~"], vec!["", "~"]; "in sorted order")]
279 #[test_case(vec!["~", ""], vec!["", "~"]; "in reversed order")]
280 fn test_empty_string_vs_tilde(original: Vec<&str>, expected: Vec<&str>) {
281 let mut list = original;
282 sort(&mut list);
283 assert_eq!(list, expected);
284 }
285
286 #[test]
287 fn test_non_digit_sorting() {
288 let mut list = vec!["aaa", "aa", "aab", "aa&", "aa_", "aa~", "a"];
289 list.sort_by(|a, b| compare_non_digit_seq(a, b));
290
291 assert_eq!(
292 list,
293 vec![
294 "a", "aa~", "aa", "aaa", "aab", "aa&", "aa_", ]
298 );
299 }
300
301 #[test]
302 fn test_non_digit_seq() {
303 let a = "file_1.txt";
304 let (seq, remainder) = non_digit_seq(a);
305 assert_eq!(seq, "file_");
306 assert_eq!(remainder, "1.txt");
307
308 let (seq, remainder) = non_digit_seq(&a[5..]);
309 assert_eq!(seq, "");
310 assert_eq!(remainder, "1.txt");
311
312 let (seq, remainder) = non_digit_seq(&a[6..]);
313 assert_eq!(seq, ".txt");
314 assert_eq!(remainder, "");
315 }
316
317 #[test]
318 fn test_unusual_test_case() {
319 let mut list = vec![
320 "a.txt", "b 1.txt", "b 10.txt", "b 11.txt", "b 5.txt", "Ssm.txt",
321 ];
322 sort(&mut list);
323
324 assert_eq!(
325 list,
326 vec!["Ssm.txt", "a.txt", "b 1.txt", "b 5.txt", "b 10.txt", "b 11.txt"]
327 );
328 }
329
330 #[test_case(
332 vec!["αβγ2.txt", "αβγ1.txt", "1αβγ.txt", "2αβγ.txt"],
333 vec!["1αβγ.txt", "2αβγ.txt", "αβγ1.txt", "αβγ2.txt"];
334 "test_with_non_ascii"
335 )]
336 fn test_with_non_utf8(original: Vec<&str>, expected: Vec<&str>) {
337 let mut list = original;
338 sort(&mut list);
339 assert_eq!(list, expected);
340 }
341
342 #[test]
343 fn test_missing_number_part() {
344 let mut original_list = vec!["file.txt", "file0.txt"];
345 sort(&mut original_list);
346
347 assert_eq!(original_list, vec!["file0.txt", "file.txt"]);
348
349 let mut original_list = vec!["file0.txt", "file.txt"];
350 sort(&mut original_list);
351
352 assert_eq!(original_list, vec!["file0.txt", "file.txt"]);
353 }
354
355 #[test_case(
361 vec!["8.10", "8.5", "8.1", "8.01", "8.010", "8.100", "8.49"],
362 vec!["8.01", "8.1", "8.5", "8.010", "8.10", "8.49", "8.100"];
363 "sort with numbers"
364 )]
365 #[test_case(
367 vec!["1.0_src.tar.gz", "1.0.5_src.tar.gz"],
368 vec!["1.0.5_src.tar.gz", "1.0_src.tar.gz"];
369 "period is before underscore"
370 )]
371 #[test_case(
373 vec!["3.0/", "3.0.5"],
374 vec!["3.0.5", "3.0/"];
375 "period is before forward slash"
376 )]
377 #[test_case(
379 vec!["a%", "az"],
380 vec!["az", "a%"];
381 "letters before non-letters"
382 )]
383 #[test_case(
385 vec!["1", "1%", "1.2", "1~", "~"],
386 vec!["~", "1~", "1", "1%", "1.2"];
387 "tilde before all others strings"
388 )]
389 #[test_case(
391 vec!["aa", "az", "a%", "aα"],
392 vec!["aa", "az", "a%", "aα"];
393 "sort ignores locale"
394 )]
395 #[test_case(
397 vec!["a", "b", ".", "c", "..", ".d20", ".d3"],
398 vec![".", "..", ".d3", ".d20", "a", "b", "c"];
399 "special directories and hidden files are sorted first"
400 )]
401 fn test_basic_tests(original: Vec<&str>, expected: Vec<&str>) {
402 let mut list = original;
403 sort(&mut list);
404 assert_eq!(list, expected);
405 }
406
407 #[test_case("hello-8.txt", ("hello-8", ".txt"); "basic")]
409 #[test_case("hello-8.2.txt", ("hello-8.2", ".txt"); "with major and minor")]
410 #[test_case("hello-8.0.12.tar.gz", ("hello-8.0.12", ".tar.gz"); "with extension")]
411 #[test_case("hello-8.2", ("hello-8.2", ""); "without extension")]
412 #[test_case("hello.foobar65", ("hello", ".foobar65"); "with long extension")]
413 #[test_case(
414 "gcc-c++-10.8.12-0.7rc2.fc9.tar.bz2",
415 ("gcc-c++-10.8.12-0.7rc2", ".fc9.tar.bz2");
416 "with multiple extensions"
417 )]
418 #[test_case(".autom4te.cfg", ("", ".autom4te.cfg"); "empty name with extension")]
419 #[test_case("a.#$%", ("a.#$%", ""); "no extension present")]
420 #[test_case("a.#$%.txt", ("a.#$%", ".txt"); "extension stops at non-alphanumeric characters")]
421 fn test_split_extension(input: &str, split: (&str, &str)) {
422 assert_eq!(split_extension(input), split);
423 }
424
425 #[test]
428 fn test_long_sorted_list() {
429 let expected = vec![
430 "",
431 ".",
432 "..",
433 ".0",
434 ".9",
435 ".A",
436 ".Z",
437 ".a~",
438 ".a",
439 ".b~",
440 ".b",
441 ".z",
442 ".zz~",
443 ".zz",
444 ".zz.~1~",
445 ".zz.0",
446 ".\u{1}",
447 ".\u{1}.txt",
448 ".\u{1}x",
449 ".\u{1}x\u{1}",
450 ".\u{1}.0",
451 "0",
452 "9",
453 "A",
454 "Z",
455 "a~",
456 "a",
457 "a.b~",
458 "a.b",
459 "a.bc~",
460 "a.bc",
461 "a+",
462 "a.",
463 "a..a",
464 "a.+",
465 "b~",
466 "b",
467 "gcc-c++-10.fc9.tar.gz",
468 "gcc-c++-10.fc9.tar.gz.~1~",
469 "gcc-c++-10.fc9.tar.gz.~2~",
470 "gcc-c++-10.8.12-0.7rc2.fc9.tar.bz2",
471 "gcc-c++-10.8.12-0.7rc2.fc9.tar.bz2.~1~",
472 "glibc-2-0.1.beta1.fc10.rpm",
473 "glibc-common-5-0.2.beta2.fc9.ebuild",
474 "glibc-common-5-0.2b.deb",
475 "glibc-common-11b.ebuild",
476 "glibc-common-11-0.6rc2.ebuild",
477 "libstdc++-0.5.8.11-0.7rc2.fc10.tar.gz",
478 "libstdc++-4a.fc8.tar.gz",
479 "libstdc++-4.10.4.20040204svn.rpm",
480 "libstdc++-devel-3.fc8.ebuild",
481 "libstdc++-devel-3a.fc9.tar.gz",
482 "libstdc++-devel-8.fc8.deb",
483 "libstdc++-devel-8.6.2-0.4b.fc8",
484 "nss_ldap-1-0.2b.fc9.tar.bz2",
485 "nss_ldap-1-0.6rc2.fc8.tar.gz",
486 "nss_ldap-1.0-0.1a.tar.gz",
487 "nss_ldap-10beta1.fc8.tar.gz",
488 "nss_ldap-10.11.8.6.20040204cvs.fc10.ebuild",
489 "z",
490 "zz~",
491 "zz",
492 "zz.~1~",
493 "zz.0",
494 "zz.0.txt",
495 "\u{1}",
496 "\u{1}.txt",
497 "\u{1}x",
498 "\u{1}x\u{1}",
499 "\u{1}.0",
500 "#\u{1}.b#",
501 "#.b#",
502 ];
503 let mut list = expected.clone();
504 list.reverse();
505 assert_ne!(list, expected);
506 sort(&mut list);
507 assert_eq!(list, expected);
508 }
509
510 #[test_case(vec!["a", "a0", "a0000"]; "zeros are the same as empty string")]
513 #[test_case(vec!["a\u{1}c-27.txt", "a\u{1}c-027.txt", "a\u{1}c-00000000000000000000000000000000000000000000000000000027.txt",]; "non-ascii")]
514 #[test_case(vec![".a\u{1}c-27.txt", ".a\u{1}c-027.txt", ".a\u{1}c-00000000000000000000000000000000000000000000000000000027.txt",]; "non-ascii with leading period")]
515 #[test_case(vec!["a\u{1}c-", "a\u{1}c-0", "a\u{1}c-00",]; "non-ascii without extension")]
516 #[test_case(vec![".a\u{1}c-", ".a\u{1}c-0", ".a\u{1}c-00",]; "non-ascii without extension and leading period")]
517 #[test_case(vec!["a\u{1}c-0.txt", "a\u{1}c-00.txt"]; "non-ascii with trailing zeros")]
518 #[test_case(vec![".a\u{1}c-1\u{1}.txt", ".a\u{1}c-001\u{1}.txt"]; "non-ascii with leading zeros before a number")]
519 fn test_strings_cmp_equal(list: Vec<&str>) {
520 let end = list.len();
521 for i in 0..end {
522 for j in (i + 1)..end {
523 assert_eq!(sequence_cmp(list[i], list[j]), Ordering::Equal);
524 }
525 }
526 }
527}