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