Skip to main content

yapi_types/
collate.rs

1//! String ordering as JavaScript's `localeCompare` gives it for identifiers and
2//! file names.
3
4use std::cmp::Ordering;
5
6/// Primary weight of a character in ICU root collation, for ASCII: whitespace,
7/// then punctuation and symbols in DUCET order, then digits, then letters
8/// without case.
9fn 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
24/// `a.localeCompare(b)` for identifiers: primary weights first, then lowercase
25/// before uppercase, then code points.
26pub 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        // Orders checked against Node's localeCompare over the whole catalog.
44        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}