1use std::cmp::Ordering;
5
6fn primary(c: char) -> (u8, u32) {
10 const PUNCTUATION: &str = "_-,;:!?.'\"()[]{}@*/\\&#%`^+<=>|~$";
11 if c.is_whitespace() {
12 (0, c as u32)
13 } else if let Some(rank) = PUNCTUATION.find(c) {
14 (1, rank as u32)
15 } else if c.is_ascii_digit() {
16 (2, c as u32)
17 } else if c.is_alphabetic() {
18 (3, c.to_lowercase().next().unwrap_or(c) as u32)
19 } else {
20 (4, c as u32)
21 }
22}
23
24pub fn locale_compare(a: &str, b: &str) -> Ordering {
27 let primaries = |text: &str| text.chars().map(primary).collect::<Vec<_>>();
28 primaries(a)
29 .cmp(&primaries(b))
30 .then_with(|| {
31 let case = |text: &str| text.chars().map(char::is_uppercase).collect::<Vec<_>>();
32 case(a).cmp(&case(b))
33 })
34 .then_with(|| a.cmp(b))
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40
41 #[test]
42 fn sorts_like_locale_compare() {
43 let mut ids = vec![
45 "gpt-4o", "gpt-4.1", "GPT-4", "gpt-4", "gpt-40", "a~b", "a:b", "a/b", "a@b", "a-b",
46 ];
47 ids.sort_by(|a, b| locale_compare(a, b));
48 assert_eq!(
49 ids,
50 [
51 "a-b", "a:b", "a@b", "a/b", "a~b", "gpt-4", "GPT-4", "gpt-4.1", "gpt-40", "gpt-4o"
52 ]
53 );
54 }
55}