1use std::cmp::Ordering;
5
6fn primary(c: char) -> (u8, u32) {
11 const PUNCTUATION: &str = "_-,;:!?.'\"()[]{}@*/\\&#%`^+<=>|~$";
12 if c.is_whitespace() {
13 (0, c as u32)
14 } else if let Some(rank) = PUNCTUATION.find(c) {
15 (1, rank as u32)
16 } else if c.is_ascii_digit() {
17 (2, c as u32)
18 } else if c.is_alphabetic() {
19 letter(c.to_lowercase().next().unwrap_or(c), 0)
20 } else {
21 (4, c as u32)
22 }
23}
24
25fn letter(letter: char, after: u8) -> (u8, u32) {
28 (3, (letter as u32) << 8 | u32::from(after))
29}
30
31enum Weight {
33 Letter(char, u8, u8),
35 Accent(u32, u8),
37}
38
39use Weight::{Accent as A, Letter as L};
40
41const LETTERS: &[(char, &[Weight])] = &[
46 ('Æ', &[L('a', 0, 10), A(0x118, 4), L('e', 0, 10)]),
47 ('Ð', &[L('d', 0, 10), A(0x118, 4)]),
48 ('Ø', &[L('o', 0, 8), A(0x2F, 2)]),
49 ('Þ', &[L('z', 1, 8)]),
50 ('ß', &[L('s', 0, 4), A(0x118, 4), L('s', 0, 4)]),
51 ('æ', &[L('a', 0, 4), A(0x118, 4), L('e', 0, 4)]),
52 ('ð', &[L('d', 0, 4), A(0x118, 4)]),
53 ('ø', &[L('o', 0, 2), A(0x2F, 2)]),
54 ('þ', &[L('z', 1, 2)]),
55 ('Đ', &[L('d', 0, 8), A(0x39, 2)]),
56 ('đ', &[L('d', 0, 2), A(0x39, 2)]),
57 ('Ħ', &[L('h', 0, 8), A(0x39, 2)]),
58 ('ħ', &[L('h', 0, 2), A(0x39, 2)]),
59 ('ı', &[L('i', 1, 2)]),
60 ('IJ', &[L('i', 0, 10), L('j', 0, 10)]),
61 ('ij', &[L('i', 0, 4), L('j', 0, 4)]),
62 ('ĸ', &[L('q', 1, 2)]),
63 ('Ŀ', &[L('l', 0, 8), A(0x118, 2)]),
64 ('ŀ', &[L('l', 0, 2), A(0x118, 2)]),
65 ('Ł', &[L('l', 0, 8), A(0x39, 2)]),
66 ('ł', &[L('l', 0, 2), A(0x39, 2)]),
67 ('ʼn', &[L('z', 2, 4), L('n', 0, 4)]),
68 ('Ŋ', &[L('n', 1, 8)]),
69 ('ŋ', &[L('n', 1, 2)]),
70 ('Œ', &[L('o', 0, 10), A(0x118, 4), L('e', 0, 10)]),
71 ('œ', &[L('o', 0, 4), A(0x118, 4), L('e', 0, 4)]),
72 ('Ŧ', &[L('t', 1, 8)]),
73 ('ŧ', &[L('t', 1, 2)]),
74 ('ſ', &[L('s', 0, 4), A(0x119, 4)]),
75 ('ẞ', &[L('s', 0, 10), A(0x118, 4), L('s', 0, 10)]),
76];
77
78const MARKS: [u8; 0x63] = [
81 0x25, 0x24, 0x27, 0x2D, 0x32, 0x3A, 0x26, 0x2E, 0x2B, 0x3B, 0x29, 0x2C, 0x28, 0x33, 0x33, 0x3C,
82 0x3D, 0x3E, 0x33, 0x22, 0x23, 0x33, 0x34, 0x34, 0x34, 0x34, 0x33, 0x3F, 0x34, 0x34, 0x34, 0x34,
83 0x34, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x30, 0x31, 0x34, 0x34, 0x34, 0x34, 0x46, 0x47, 0x34,
84 0x48, 0x49, 0x21, 0x34, 0x4A, 0x39, 0x35, 0x35, 0x2F, 0x4B, 0x34, 0x34, 0x34, 0x33, 0x33, 0x33,
85 0x25, 0x24, 0x2A, 0x22, 0x2B, 0x4C, 0x33, 0x34, 0x34, 0x34, 0x33, 0x33, 0x33, 0x34, 0x34, 0x0,
86 0x33, 0x33, 0x33, 0x34, 0x34, 0x34, 0x34, 0x33, 0x4D, 0x34, 0x34, 0x33, 0x34, 0x33, 0x33, 0x34,
87 0x4E, 0x4F, 0x34,
88];
89
90const COMMON: (u32, u8) = (0x20, 2);
92
93type Element = (Option<(u8, u32)>, u32, u8);
96
97fn elements(text: &str) -> Vec<Element> {
100 use unicode_normalization::UnicodeNormalization;
101 let mut elements = Vec::new();
102 for c in text.nfd() {
103 if let Ok(index) = LETTERS.binary_search_by_key(&c, |(letter, _)| *letter) {
104 elements.extend(LETTERS[index].1.iter().map(|weight| match *weight {
105 L(base, after, tertiary) => (Some(letter(base, after)), COMMON.0, tertiary),
106 A(secondary, tertiary) => (None, secondary, tertiary),
107 }));
108 } else if unicode_normalization::char::is_combining_mark(c) {
109 let secondary = (c as usize)
111 .checked_sub(0x300)
112 .and_then(|index| MARKS.get(index))
113 .map_or(0x200 + c as u32, |&weight| u32::from(weight));
114 if secondary != 0 {
115 elements.push((None, secondary, COMMON.1));
116 }
117 } else {
118 let tertiary = if c.is_uppercase() { 8 } else { COMMON.1 };
119 elements.push((Some(primary(c)), COMMON.0, tertiary));
120 }
121 }
122 elements
123}
124
125pub fn locale_compare(a: &str, b: &str) -> Ordering {
130 let (a_elements, b_elements) = (elements(a), elements(b));
131 let primaries = |elements: &[Element]| elements.iter().filter_map(|e| e.0).collect::<Vec<_>>();
132 let secondaries = |elements: &[Element]| elements.iter().map(|e| e.1).collect::<Vec<_>>();
133 let tertiaries = |elements: &[Element]| elements.iter().map(|e| e.2).collect::<Vec<_>>();
134 primaries(&a_elements)
135 .cmp(&primaries(&b_elements))
136 .then_with(|| secondaries(&a_elements).cmp(&secondaries(&b_elements)))
137 .then_with(|| tertiaries(&a_elements).cmp(&tertiaries(&b_elements)))
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn sorts_like_locale_compare() {
146 let mut ids = vec![
150 "gpt-4o",
151 "gpt-4.1",
152 "GPT-4",
153 "gpt-4",
154 "gpt-40",
155 "a~b",
156 "a:b",
157 "a/b",
158 "a@b",
159 "a-b",
160 "Zed.txt",
161 "ünï cödé.txt",
162 "été",
163 "Été",
164 "ete",
165 "être",
166 "côté",
167 "cote",
168 "coté",
169 "côte",
170 "e\u{301}tf",
171 "Ångström",
172 "angle",
173 "ørsted",
174 "oz",
175 "straße",
176 "strasse",
177 "stras",
178 "Łódź",
179 "lz",
180 "æble",
181 "aeble",
182 "afar",
183 "þorn",
184 "zebra",
185 "ŋa",
186 "nz",
187 "đuro",
188 "dz",
189 ];
190 ids.sort_by(|a, b| locale_compare(a, b));
191 assert_eq!(
192 ids,
193 [
194 "a-b",
195 "a:b",
196 "a@b",
197 "a/b",
198 "a~b",
199 "aeble",
200 "æble",
201 "afar",
202 "angle",
203 "Ångström",
204 "cote",
205 "coté",
206 "côte",
207 "côté",
208 "đuro",
209 "dz",
210 "ete",
211 "été",
212 "Été",
213 "e\u{301}tf",
214 "être",
215 "gpt-4",
216 "GPT-4",
217 "gpt-4.1",
218 "gpt-40",
219 "gpt-4o",
220 "Łódź",
221 "lz",
222 "nz",
223 "ŋa",
224 "ørsted",
225 "oz",
226 "stras",
227 "strasse",
228 "straße",
229 "ünï cödé.txt",
230 "zebra",
231 "Zed.txt",
232 "þorn"
233 ]
234 );
235 assert_eq!(locale_compare("đ", "d\u{335}"), Ordering::Equal);
237 assert!(LETTERS.windows(2).all(|pair| pair[0].0 < pair[1].0));
238 }
239}