Skip to main content

ui/
i18n_collate.rs

1// i18n_collate -- sorting by language (collation) and Unicode case mapping
2//
3//   collate(a, b) -> Ordering           two texts in the order of the user's language
4//                                       (locale_tag(): the format locale, as Windows'
5//                                       CompareStringEx): base letters first, then accents,
6//                                       then case: "a" < "A" < "á" < "ä" < "b"
7//   collator() / Collator::new(tag)     a collator with options: .strength(COLL_PRIMARY ..
8//                                       COLL_IDENTICAL), .numeric(true) ("Datei 2" before
9//                                       "Datei 10"), .phonebook(true) (German phone book:
10//                                       ä = ae); .compare(a, b), .key(s) (a sort key: keys
11//                                       compare like their texts), .sort(items, text).  The
12//                                       tag may carry "-u-co-phonebk", "-u-kn", "-u-ks-level1"
13//   Locale::collate(a, b) / collator()  the same for a fixed region
14//   to_upper(s) / to_lower(s) / fold(s) case mapping of all of Unicode in the user's language:
15//                                       ß -> SS, "ΟΔΟΣ" -> "οδος" (final sigma), Turkish and
16//                                       Azeri i / İ and ı / I; fold() for comparisons (ß and
17//                                       ẞ -> ss, ς -> σ)
18//   to_upper_in(lang, s) / to_lower_in / fold_in   the same for a language
19//   eq_ignore_case(a, b)                equal except for case (also composed against
20//                                       decomposed accents: "É" = "e\u{301}")
21//
22// The collation is a compact form of the Unicode Collation Algorithm, not the CLDR data:
23// every character becomes collation elements (primary: the letter, secondary: its accents,
24// tertiary: case and variants such as ß or ſ); the canonical decompositions come from the
25// generated table i18n_unicode.rs (tools/gen_unicode.py).  Order of the primaries: spaces,
26// punctuation and symbols (by code point), digits (by value), Latin letters a .. z, then the
27// letters of the other scripts by code point (Greek, Cyrillic, Hebrew, Arabic, ..., kana as
28// hiragana, CJK).  Language rules where they are usual: German (ä like a; phone book ä = ae),
29// Swedish / Finnish (å ä ö after z), Danish / Norwegian (æ ø å after z, aa = å), Spanish
30// (ñ after n), Czech / Slovak (č ř š ž, ch after h), Polish (ą ć ę ł ń ó ś ź ż), Turkish
31// (ç ğ ı ö ş ü, I is the capital of ı).  Everywhere: ß = ss, æ = ae, œ = oe, ø ł đ like o l d
32// with a secondary difference, ð þ ŋ ı letters of their own, Cyrillic і ї й ґ є ў ђ ј љ њ ћ
33// џ in their alphabetical places, Hebrew final forms as variants.
34//
35// core and alloc bring the default case mappings of all of Unicode (char::to_uppercase,
36// str::to_lowercase with the final sigma); this module adds the language rules (Turkish,
37// Azeri) and case folding.  Not done: Lithuanian dot rules, Greek upper case without accents.
38
39use alloc::string::String;
40use alloc::vec::Vec;
41use core::cmp::Ordering;
42
43#[path = "i18n_unicode.rs"]
44mod tables;
45use tables::{COMPAT, DECOMP, MARKS, ZEROS};
46
47use super::Locale;
48
49/// collation strength: base letters only ("a" = "A" = "ä")
50pub const COLL_PRIMARY: u32 = 1;
51/// collation strength: base letters and accents ("a" = "A" < "ä"): sorting without case
52pub const COLL_SECONDARY: u32 = 2;
53/// collation strength: base letters, accents and case ("a" < "A" < "ä"; the default)
54pub const COLL_TERTIARY: u32 = 3;
55/// collation strength: as COLL_TERTIARY, then the code points (only equal texts compare equal)
56pub const COLL_IDENTICAL: u32 = 4;
57
58// primary weights (0: ignorable); Latin letters 32 apart, the others 4 apart, so that a
59// language can put letters of its own after (or before) them
60const P_SPACE: u32 = 0x0100_0000;
61const P_PUNCT: u32 = 0x0200_0000;
62const P_DIGIT: u32 = 0x0400_0000;
63const P_NUMBER: u32 = P_DIGIT + 0x100; // numeric: the count of digits comes first
64const P_LATIN: u32 = 0x0800_0000;
65const P_OTHER: u32 = 0x1000_0000;
66
67// secondary weights: the letter itself, the accents (MARK_ORDER, then by code point), strokes
68const S_COMMON: u32 = 1;
69const S_STROKE: u32 = 0x40;
70const S_LIG: u32 = 0x41;
71// the accents in the order of the UCA's secondary weights
72const MARK_ORDER: [char; 16] = [
73    '\u{301}', '\u{300}', '\u{306}', '\u{302}', '\u{30C}', '\u{30A}', '\u{308}', '\u{30B}', '\u{303}', '\u{307}', '\u{327}', '\u{328}',
74    '\u{304}', '\u{323}', '\u{331}', '\u{313}',
75];
76const DIAER: u32 = mark_weight('\u{308}');
77
78const fn mark_weight(m: char) -> u32 {
79    let mut k = 0;
80    while k < MARK_ORDER.len() {
81        if MARK_ORDER[k] == m {
82            return 2 + k as u32;
83        }
84        k += 1;
85    }
86    0x100 + m as u32
87}
88
89// a letter of a language rule: the primary of `base` moved by `step` (0: the letter itself),
90// an optional secondary weight after it, a tertiary variant (Danish "aa", ß), an optional
91// second letter (expansions: ß = ss, phone book ä = ae)
92#[derive(Clone, Copy)]
93struct Tail {
94    base: char,
95    step: i8,
96    sec: u32,
97    var: bool,
98    then: char,
99}
100
101// a letter of its own `step` places after (before) base
102const fn at(base: char, step: i8) -> Tail {
103    Tail { base, step, sec: 0, var: false, then: '\0' }
104}
105// base with a secondary difference
106const fn like(base: char, sec: u32) -> Tail {
107    Tail { base, step: 0, sec, var: false, then: '\0' }
108}
109// a tertiary variant of base (final forms)
110const fn var(base: char) -> Tail {
111    Tail { base, step: 0, sec: 0, var: true, then: '\0' }
112}
113// two letters: a, the secondary weight, b
114const fn exp(a: char, sec: u32, b: char, var: bool) -> Tail {
115    Tail { base: a, step: 0, sec, var, then: b }
116}
117
118// the patterns are lower case and decomposed (NFD)
119type Rules = &'static [(&'static str, Tail)];
120
121// every language: letters without a decomposition
122static ROOT: Rules = &[
123    ("ß", exp('s', 0, 's', true)),
124    ("æ", exp('a', 0, 'e', true)),
125    ("œ", exp('o', 0, 'e', true)),
126    ("ð", at('d', 1)),
127    ("ə", at('e', 1)),
128    ("ı", at('i', 1)),
129    ("ŋ", at('n', 1)),
130    ("þ", at('z', 1)),
131    ("ø", like('o', S_STROKE)),
132    ("ł", like('l', S_STROKE)),
133    ("đ", like('d', S_STROKE)),
134    ("ħ", like('h', S_STROKE)),
135    ("ŧ", like('t', S_STROKE)),
136    ("ƀ", like('b', S_STROKE)),
137    ("ɨ", like('i', S_STROKE)),
138    ("ƶ", like('z', S_STROKE)),
139    ("ǥ", like('g', S_STROKE)),
140    // Cyrillic letters of Ukrainian, Belarusian, Serbian, Macedonian in their places
141    ("ґ", at('г', 1)),
142    ("є", at('е', 1)),
143    ("і", at('и', 1)),
144    ("і\u{308}", at('и', 2)),
145    ("и\u{306}", at('й', 0)),
146    ("ђ", at('д', 1)),
147    ("ѕ", at('з', 1)),
148    ("ј", at('й', 1)),
149    ("љ", at('л', 1)),
150    ("њ", at('н', 1)),
151    ("ћ", at('т', 1)),
152    ("у\u{306}", at('у', 1)),
153    ("џ", at('ч', 1)),
154    // Hebrew final forms
155    ("ך", var('כ')),
156    ("ם", var('מ')),
157    ("ן", var('נ')),
158    ("ף", var('פ')),
159    ("ץ", var('צ')),
160];
161
162static DE_PHONEBOOK: Rules = &[("a\u{308}", exp('a', DIAER, 'e', false)), ("o\u{308}", exp('o', DIAER, 'e', false)), ("u\u{308}", exp('u', DIAER, 'e', false))];
163
164static SV: Rules = &[
165    ("a\u{30A}", at('z', 1)),
166    ("a\u{308}", at('z', 2)),
167    ("æ", Tail { sec: S_LIG, ..at('z', 2) }),
168    ("o\u{308}", at('z', 3)),
169    ("ø", Tail { sec: S_STROKE, ..at('z', 3) }),
170    ("u\u{308}", like('y', DIAER)),
171];
172
173static FI: Rules = &[
174    ("a\u{30A}", at('z', 1)),
175    ("a\u{308}", at('z', 2)),
176    ("æ", Tail { sec: S_LIG, ..at('z', 2) }),
177    ("o\u{308}", at('z', 3)),
178    ("ø", Tail { sec: S_STROKE, ..at('z', 3) }),
179    ("o\u{303}", Tail { sec: mark_weight('\u{303}'), ..at('z', 3) }),
180    ("u\u{308}", like('y', DIAER)),
181];
182
183static DA: Rules = &[
184    ("æ", at('z', 1)),
185    ("a\u{308}", Tail { sec: DIAER, ..at('z', 1) }),
186    ("ø", at('z', 2)),
187    ("o\u{308}", Tail { sec: DIAER, ..at('z', 2) }),
188    ("a\u{30A}", at('z', 3)),
189    ("aa", Tail { var: true, ..at('z', 3) }),
190    ("u\u{308}", like('y', DIAER)),
191];
192
193static ES: Rules = &[("n\u{303}", at('n', 1))];
194
195static CS: Rules = &[("c\u{30C}", at('c', 1)), ("ch", at('h', 1)), ("r\u{30C}", at('r', 1)), ("s\u{30C}", at('s', 1)), ("z\u{30C}", at('z', 1))];
196
197static SK: Rules = &[
198    ("a\u{308}", at('a', 1)),
199    ("c\u{30C}", at('c', 1)),
200    ("ch", at('h', 1)),
201    ("o\u{302}", at('o', 1)),
202    ("r\u{30C}", at('r', 1)),
203    ("s\u{30C}", at('s', 1)),
204    ("z\u{30C}", at('z', 1)),
205];
206
207static PL: Rules = &[
208    ("a\u{328}", at('a', 1)),
209    ("c\u{301}", at('c', 1)),
210    ("e\u{328}", at('e', 1)),
211    ("ł", at('l', 1)),
212    ("n\u{301}", at('n', 1)),
213    ("o\u{301}", at('o', 1)),
214    ("s\u{301}", at('s', 1)),
215    ("z\u{301}", at('z', 1)),
216    ("z\u{307}", at('z', 2)),
217];
218
219static TR: Rules = &[
220    ("c\u{327}", at('c', 1)),
221    ("g\u{306}", at('g', 1)),
222    ("ı", at('i', -1)),
223    ("o\u{308}", at('o', 1)),
224    ("s\u{327}", at('s', 1)),
225    ("u\u{308}", at('u', 1)),
226];
227
228// the language part of a tag ("de-CH" -> "de"; old codes as today's)
229fn language(tag: &str) -> &str {
230    let l = tag.split(['-', '_', '.', '@']).next().unwrap_or("");
231    if l.eq_ignore_ascii_case("no") || l.eq_ignore_ascii_case("nn") { "nb" } else { l }
232}
233
234fn turkic(tag: &str) -> bool {
235    let l = language(tag);
236    l.eq_ignore_ascii_case("tr") || l.eq_ignore_ascii_case("az")
237}
238
239/// compares texts in the order of a language (see the module comment); copy it freely
240#[derive(Clone, Copy)]
241pub struct Collator {
242    rules: Rules,
243    german: bool,
244    turkish: bool,
245    phonebook: bool,
246    numeric: bool,
247    strength: u32,
248}
249
250impl Collator {
251    /// the collator of a language (a BCP 47 tag or a POSIX name; unknown languages sort by the
252    /// common rules); "-u-co-phonebk", "-u-kn" and "-u-ks-level1 / 2 / 3 / identic" set the
253    /// options as in ICU, also "de-DE_phoneb" (Windows) and "@collation=phonebook" (POSIX)
254    pub fn new(tag: &str) -> Collator {
255        let l = language(tag).to_ascii_lowercase();
256        let rules: Rules = match l.as_str() {
257            "sv" => SV,
258            "fi" => FI,
259            "da" | "nb" => DA,
260            "es" => ES,
261            "cs" => CS,
262            "sk" => SK,
263            "pl" => PL,
264            "tr" => TR,
265            _ => &[],
266        };
267        let mut c = Collator { rules, german: l == "de", turkish: l == "tr" || l == "az", phonebook: false, numeric: false, strength: COLL_TERTIARY };
268        let low = tag.to_ascii_lowercase();
269        let parts: Vec<&str> = low.split(['-', '_']).collect();
270        c.phonebook = low.contains("collation=phonebook") || parts.contains(&"phoneb");
271        if let Some(u) = parts.iter().position(|p| *p == "u") {
272            let ext = &parts[u + 1..];
273            for (k, key) in ext.iter().enumerate() {
274                let val = ext.get(k + 1).copied().filter(|v| v.len() > 2).unwrap_or("");
275                match *key {
276                    "co" => c.phonebook = val == "phonebk",
277                    "kn" => c.numeric = val != "false",
278                    "ks" => {
279                        c.strength = match val {
280                            "level1" => COLL_PRIMARY,
281                            "level2" => COLL_SECONDARY,
282                            "identic" => COLL_IDENTICAL,
283                            _ => COLL_TERTIARY,
284                        }
285                    }
286                    _ => {}
287                }
288            }
289        }
290        c
291    }
292
293    /// the same collator with another strength (COLL_PRIMARY .. COLL_IDENTICAL)
294    pub fn strength(mut self, s: u32) -> Collator {
295        self.strength = s.clamp(COLL_PRIMARY, COLL_IDENTICAL);
296        self
297    }
298
299    /// numbers by their value: "Datei 2" before "Datei 10", "007" = "7" up to the tertiary level
300    pub fn numeric(mut self, on: bool) -> Collator {
301        self.numeric = on;
302        self
303    }
304
305    /// German phone book order (ä = ae, ö = oe, ü = ue); no effect in other languages
306    pub fn phonebook(mut self, on: bool) -> Collator {
307        self.phonebook = on;
308        self
309    }
310
311    /// compare two texts
312    pub fn compare(&self, a: &str, b: &str) -> Ordering {
313        if a == b {
314            return Ordering::Equal;
315        }
316        self.key(a).cmp(&self.key(b))
317    }
318
319    /// the sort key of a text: keys compare as their texts do (sort_by_cached_key, indexes)
320    pub fn key(&self, s: &str) -> Vec<u32> {
321        let ces = self.elements(s);
322        let mut k: Vec<u32> = ces.iter().filter(|e| e.p != 0).map(|e| e.p).collect();
323        if self.strength >= COLL_SECONDARY {
324            k.push(0);
325            k.extend(ces.iter().filter(|e| e.s != 0).map(|e| e.s));
326        }
327        if self.strength >= COLL_TERTIARY {
328            k.push(0);
329            k.extend(ces.iter().filter(|e| e.p != 0).map(|e| e.t));
330        }
331        if self.strength >= COLL_IDENTICAL {
332            k.push(0);
333            k.extend(s.chars().map(|c| c as u32 + 1));
334        }
335        k
336    }
337
338    /// sort items by a text of each (stable; each key is computed once)
339    pub fn sort<T>(&self, items: &mut [T], text: impl Fn(&T) -> &str) {
340        items.sort_by_cached_key(|x| self.key(text(x)));
341    }
342
343    fn rules(&self) -> Rules {
344        if self.german && self.phonebook { DE_PHONEBOOK } else { self.rules }
345    }
346
347    // the text lower case and decomposed, each letter with its case and variant flag
348    fn chars(&self, s: &str) -> Vec<Ch> {
349        let mut out: Vec<Ch> = Vec::with_capacity(s.len());
350        for c in s.chars() {
351            let (c, var) = match c as u32 {
352                0xFF01..=0xFF5E => (char::from_u32(c as u32 - 0xFEE0).unwrap_or(c), true), // fullwidth ASCII
353                0x30A1..=0x30F6 => (char::from_u32(c as u32 - 0x60).unwrap_or(c), true),   // katakana as hiragana
354                _ => (c, false),
355            };
356            if let Some(letters) = compat(c) {
357                for x in letters.chars() {
358                    self.push_char(&mut out, x, true);
359                }
360            } else {
361                self.push_char(&mut out, c, var);
362            }
363        }
364        out
365    }
366
367    fn push_char(&self, out: &mut Vec<Ch>, c: char, var: bool) {
368        if self.turkish && (c == 'I' || c == 'İ') {
369            out.push(Ch { c: if c == 'I' { 'ı' } else { 'i' }, upper: true, var });
370            return;
371        }
372        let upper = c.to_lowercase().next() != Some(c);
373        for l in c.to_lowercase() {
374            // letters that fold to another one (ς -> σ, ſ -> s, µ -> μ): variants of it
375            let (l, v) = match single(l.to_uppercase()).and_then(|u| single(u.to_lowercase())) {
376                Some(f) if f != l && l != 'ı' => (f, true),
377                _ => (l, false),
378            };
379            let mut first = true;
380            decompose(l, &mut |d| {
381                out.push(if first { Ch { c: d, upper, var: var || v } } else { Ch { c: d, upper: false, var: false } });
382                first = false;
383            });
384        }
385    }
386
387    // the collation elements of a text
388    fn elements(&self, s: &str) -> Vec<Ce> {
389        let chs = self.chars(s);
390        let mut ces: Vec<Ce> = Vec::with_capacity(chs.len() + 4);
391        let mut i = 0;
392        while i < chs.len() {
393            if let Some((n, t)) = rule_at(self.rules(), &chs[i..]).or_else(|| rule_at(ROOT, &chs[i..])) {
394                let tert = tertiary(chs[i].upper, chs[i].var || t.var);
395                ces.push(Ce { p: primary_of(t.base).wrapping_add_signed(t.step as i32), s: S_COMMON, t: tert });
396                if t.sec != 0 {
397                    ces.push(Ce { p: 0, s: t.sec, t: 0 });
398                }
399                if t.then != '\0' {
400                    ces.push(Ce { p: primary_of(t.then), s: S_COMMON, t: tert });
401                }
402                i += n;
403                continue;
404            }
405            let ch = chs[i];
406            i += 1;
407            if is_mark(ch.c) {
408                ces.push(Ce { p: 0, s: mark_weight(ch.c), t: 0 });
409            } else if ch.c.is_whitespace() {
410                ces.push(Ce { p: P_SPACE + ch.c as u32, s: S_COMMON, t: tertiary(false, ch.var) });
411            } else if ignorable(ch.c) {
412                // soft hyphen, joiners, direction marks, controls
413            } else if let Some(d) = digit(ch.c) {
414                if !self.numeric {
415                    ces.push(Ce { p: P_DIGIT + d, s: S_COMMON, t: tertiary(false, ch.var || !ch.c.is_ascii_digit()) });
416                    continue;
417                }
418                // a whole number: the count of its digits, then the digits (leading zeros count
419                // only at the tertiary level)
420                let mut digits = alloc::vec![(d, ch)];
421                while let Some(d) = chs.get(i).and_then(|c| digit(c.c).map(|d| (d, *c))) {
422                    digits.push(d);
423                    i += 1;
424                }
425                let zeros = digits.iter().take(digits.len() - 1).take_while(|d| d.0 == 0).count();
426                let sig = &digits[zeros..];
427                ces.push(Ce { p: P_NUMBER + sig.len() as u32, s: S_COMMON, t: 1 + zeros as u32 });
428                for (d, c) in sig {
429                    ces.push(Ce { p: P_DIGIT + d, s: S_COMMON, t: tertiary(false, c.var || !c.c.is_ascii_digit()) });
430                }
431            } else {
432                let p = if ch.c.is_alphabetic() { primary_of(ch.c) } else { P_PUNCT + ch.c as u32 };
433                ces.push(Ce { p, s: S_COMMON, t: tertiary(ch.upper, ch.var) });
434            }
435        }
436        ces
437    }
438}
439
440// one character of a text after case mapping and decomposition
441#[derive(Clone, Copy)]
442struct Ch {
443    c: char,
444    upper: bool,
445    var: bool,
446}
447
448// a collation element: primary (0: ignorable), secondary, tertiary weight
449struct Ce {
450    p: u32,
451    s: u32,
452    t: u32,
453}
454
455// lower case 1, its variants 2, upper case 3, upper case variants 4
456fn tertiary(upper: bool, var: bool) -> u32 {
457    1 + var as u32 + 2 * upper as u32
458}
459
460// the primary weight of a letter
461fn primary_of(c: char) -> u32 {
462    match c {
463        'a'..='z' => P_LATIN + (c as u32 - 'a' as u32 + 1) * 32,
464        _ => P_OTHER + c as u32 * 4,
465    }
466}
467
468// the longest pattern of the rules at the start of chs -> (its length, the letter)
469fn rule_at(rules: Rules, chs: &[Ch]) -> Option<(usize, Tail)> {
470    let mut best: Option<(usize, Tail)> = None;
471    for (pat, t) in rules {
472        let mut n = 0;
473        let ok = pat.chars().all(|p| {
474            let hit = chs.get(n).is_some_and(|c| c.c == p);
475            n += 1;
476            hit
477        });
478        if ok && best.is_none_or(|b| n > b.0) {
479            best = Some((n, *t));
480        }
481    }
482    best
483}
484
485fn single(mut it: impl Iterator<Item = char>) -> Option<char> {
486    let c = it.next()?;
487    if it.next().is_none() { Some(c) } else { None }
488}
489
490// the canonical decomposition of c, recursively (DECOMP: one step each)
491fn decompose(c: char, out: &mut impl FnMut(char)) {
492    let cp = c as u32;
493    if cp >= 0xC0 && cp <= 0xFFFF {
494        if let Ok(k) = DECOMP.binary_search_by_key(&(cp as u16), |e| e.0) {
495            let (_, a, b) = DECOMP[k];
496            decompose(char::from_u32(a as u32).unwrap_or(c), out);
497            if b != 0 {
498                decompose(char::from_u32(b as u32).unwrap_or(c), out);
499            }
500            return;
501        }
502    }
503    out(c);
504}
505
506// the letters of a compatibility character (ij, fi, ª, ²)
507fn compat(c: char) -> Option<&'static str> {
508    let cp = c as u32;
509    if !(0xAA..=0xFFFF).contains(&cp) {
510        return None;
511    }
512    COMPAT.binary_search_by_key(&(cp as u16), |e| e.0).ok().map(|k| COMPAT[k].1)
513}
514
515fn is_mark(c: char) -> bool {
516    let cp = c as u32;
517    if !(0x300..=0xFFFF).contains(&cp) {
518        return false;
519    }
520    let k = MARKS.partition_point(|r| (r.1 as u32) < cp);
521    k < MARKS.len() && MARKS[k].0 as u32 <= cp
522}
523
524fn ignorable(c: char) -> bool {
525    c.is_control()
526        || matches!(c, '\u{AD}' | '\u{34F}' | '\u{61C}' | '\u{180E}' | '\u{200B}'..='\u{200F}' | '\u{202A}'..='\u{202E}'
527            | '\u{2060}'..='\u{206F}' | '\u{FE00}'..='\u{FE0F}' | '\u{FEFF}')
528}
529
530// the value of a decimal digit of any script
531fn digit(c: char) -> Option<u32> {
532    let cp = c as u32;
533    if c.is_ascii_digit() {
534        return Some(cp - '0' as u32);
535    }
536    if cp < 0x660 {
537        return None;
538    }
539    let k = ZEROS.partition_point(|&z| z <= cp);
540    (k > 0 && cp - ZEROS[k - 1] <= 9).then(|| cp - ZEROS[k - 1])
541}
542
543/// the user's collator (the language of locale_tag(), tertiary strength)
544pub fn collator() -> Collator {
545    Collator::new(&super::locale_tag())
546}
547
548/// compare two texts in the order of the user's language (see the module comment)
549pub fn collate(a: &str, b: &str) -> Ordering {
550    collator().compare(a, b)
551}
552
553impl Locale {
554    /// compare two texts in the order of the region's language
555    pub fn collate(&self, a: &str, b: &str) -> Ordering {
556        self.collator().compare(a, b)
557    }
558    /// the collator of the region's language
559    pub fn collator(&self) -> Collator {
560        Collator::new(self.tag())
561    }
562}
563
564// ---------------------------------------------------------------- case
565
566/// upper case in the user's language: "straße" -> "STRASSE", Turkish "i" -> "İ"
567pub fn to_upper(s: &str) -> String {
568    to_upper_in(&super::locale_tag(), s)
569}
570
571/// lower case in the user's language: "ΟΔΟΣ" -> "οδος", Turkish "I" -> "ı"
572pub fn to_lower(s: &str) -> String {
573    to_lower_in(&super::locale_tag(), s)
574}
575
576/// case folding in the user's language (for comparisons, not for display): "Straße" ->
577/// "strasse", "ΣΟΦΟΣ" and "σοφος" -> "σοφοσ"
578pub fn fold(s: &str) -> String {
579    fold_in(&super::locale_tag(), s)
580}
581
582/// equal except for case in the user's language ("STRASSE" = "Straße"); composed and
583/// decomposed accents are equal too
584pub fn eq_ignore_case(a: &str, b: &str) -> bool {
585    a == b || nfd(&fold(a)) == nfd(&fold(b))
586}
587
588/// upper case in a language (BCP 47 tag)
589pub fn to_upper_in(lang: &str, s: &str) -> String {
590    if turkic(lang) && s.contains('i') { s.replace('i', "İ").to_uppercase() } else { s.to_uppercase() }
591}
592
593/// lower case in a language (BCP 47 tag)
594pub fn to_lower_in(lang: &str, s: &str) -> String {
595    if turkic(lang) && s.contains(['I', 'İ']) {
596        s.replace("I\u{307}", "i").replace('I', "ı").replace('İ', "i").to_lowercase()
597    } else {
598        s.to_lowercase()
599    }
600}
601
602/// case folding in a language (BCP 47 tag): the full folding of Unicode's CaseFolding.txt,
603/// Turkish and Azeri with their I / ı and İ / i
604pub fn fold_in(lang: &str, s: &str) -> String {
605    let tr = turkic(lang);
606    let mut out = String::with_capacity(s.len());
607    for c in s.chars() {
608        match c {
609            'I' if tr => out.push('ı'),
610            'İ' if tr => out.push('i'),
611            'ı' => out.push(c),
612            // Cherokee folds to its capitals
613            '\u{13A0}'..='\u{13F5}' | '\u{13F8}'..='\u{13FD}' | '\u{AB70}'..='\u{ABBF}' => out.extend(c.to_uppercase()),
614            _ => {
615                for u in c.to_uppercase() {
616                    for l in u.to_lowercase() {
617                        if l == 'ß' { out.push_str("ss") } else { out.push(l) }
618                    }
619                }
620            }
621        }
622    }
623    out
624}
625
626// the canonical decomposition of a text (without reordering the marks)
627fn nfd(s: &str) -> String {
628    let mut out = String::with_capacity(s.len());
629    for c in s.chars() {
630        decompose(c, &mut |d| out.push(d));
631    }
632    out
633}