Skip to main content

ferroui_base/utilities/
compare_info.rs

1//! String comparison: the counterparts of .NET's `StringComparison`,
2//! `CompareOptions` and `CultureInfo.CompareInfo`.
3//!
4//! The ordinal comparisons are complete and built in. The culture-sensitive
5//! comparisons belong to the culture: [`CultureInfo::compare_info`] gives the
6//! comparer of a culture, whose rules come from the culture data provider
7//! registered with the locator ([`ICultureDataProvider::get_compare_rules`]).
8//! Without a provider, or for a culture the provider has no rules for, the
9//! built-in invariant rules are used. They are the ones the reference
10//! runtime uses in its invariant globalization mode: a culture-sensitive
11//! comparison is an ordinal one, and a culture-sensitive comparison that
12//! ignores case is an ordinal one that ignores case. They know no ignorable
13//! characters, expansions, contractions or canonical equivalence; a provider
14//! that has collation data supplies them.
15//!
16//! Positions and lengths are in UTF-16 code units, as in the reference.
17
18use super::{CultureInfo, ICultureDataProvider};
19use crate::{FerroLocator, LocatorExtensions};
20use bitflags::bitflags;
21use std::cmp::Ordering;
22use std::rc::Rc;
23
24bitflags! {
25    /// The string comparison options to use with [`CompareInfo`].
26    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27    pub struct CompareOptions: i32 {
28        /// The default option settings for string comparisons.
29        const NONE = 0;
30        /// The string comparison must ignore case.
31        const IGNORE_CASE = 0x1;
32        /// The string comparison must use successive UTF-16 code units,
33        /// upper-cased with the invariant simple case mapping.
34        const ORDINAL_IGNORE_CASE = 0x1000_0000;
35        /// The string comparison must use successive UTF-16 code units.
36        const ORDINAL = 0x4000_0000;
37    }
38}
39
40/// Specifies the culture, case, and sort rules to be used by the comparisons
41/// of strings.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[repr(i32)]
44pub enum StringComparison {
45    /// Compare strings using culture-sensitive sort rules and the current
46    /// culture.
47    CurrentCulture = 0,
48    /// Compare strings using culture-sensitive sort rules, the current
49    /// culture, and ignoring the case of the strings being compared.
50    CurrentCultureIgnoreCase = 1,
51    /// Compare strings using culture-sensitive sort rules and the invariant
52    /// culture.
53    InvariantCulture = 2,
54    /// Compare strings using culture-sensitive sort rules, the invariant
55    /// culture, and ignoring the case of the strings being compared.
56    InvariantCultureIgnoreCase = 3,
57    /// Compare strings using ordinal (binary) sort rules.
58    Ordinal = 4,
59    /// Compare strings using ordinal (binary) sort rules and ignoring the
60    /// case of the strings being compared.
61    OrdinalIgnoreCase = 5,
62}
63
64impl StringComparison {
65    /// The comparer and the options this comparison stands for.
66    fn resolve(self) -> (CompareInfo, CompareOptions) {
67        match self {
68            StringComparison::CurrentCulture => (CultureInfo::current_culture().compare_info(), CompareOptions::NONE),
69            StringComparison::CurrentCultureIgnoreCase => {
70                (CultureInfo::current_culture().compare_info(), CompareOptions::IGNORE_CASE)
71            }
72            StringComparison::InvariantCulture => (CompareInfo::invariant(), CompareOptions::NONE),
73            StringComparison::InvariantCultureIgnoreCase => (CompareInfo::invariant(), CompareOptions::IGNORE_CASE),
74            StringComparison::Ordinal => (CompareInfo::invariant(), CompareOptions::ORDINAL),
75            StringComparison::OrdinalIgnoreCase => (CompareInfo::invariant(), CompareOptions::ORDINAL_IGNORE_CASE),
76        }
77    }
78
79    /// Compares two strings (`string.Compare(a, b, comparison)`).
80    pub fn compare(self, a: &str, b: &str) -> Ordering {
81        let (compare_info, options) = self.resolve();
82        compare_info.compare(a, b, options)
83    }
84
85    /// Whether two strings, either of which may be null, are equal
86    /// (`string.Equals(a, b, comparison)`).
87    pub fn equals(self, a: Option<&str>, b: Option<&str>) -> bool {
88        match (a, b) {
89            (None, None) => true,
90            (Some(a), Some(b)) => self.compare(a, b) == Ordering::Equal,
91            _ => false,
92        }
93    }
94
95    /// Whether `source` starts with `prefix` (`source.StartsWith(prefix,
96    /// comparison)`).
97    pub fn starts_with(self, source: &str, prefix: &str) -> bool {
98        let (compare_info, options) = self.resolve();
99        compare_info.is_prefix(source, prefix, options)
100    }
101
102    /// The position of the first occurrence of `value` in `source` in
103    /// UTF-16 code units, or -1 (`source.IndexOf(value, comparison)`).
104    pub fn index_of(self, source: &str, value: &str) -> i32 {
105        let (compare_info, options) = self.resolve();
106        compare_info.index_of(source, value, options)
107    }
108
109    /// Whether `value` occurs in `source` (`source.Contains(value,
110    /// comparison)`).
111    pub fn contains(self, source: &str, value: &str) -> bool {
112        self.index_of(source, value) >= 0
113    }
114}
115
116/// The culture-sensitive comparison rules of a culture, supplied by a
117/// culture data provider. `options` never has an ordinal flag: the ordinal
118/// comparisons do not reach the rules of a culture.
119pub trait ICompareRules: 'static {
120    /// Compares two strings.
121    fn compare(&self, a: &str, b: &str, options: CompareOptions) -> Ordering;
122
123    /// Whether `source` starts with `prefix`.
124    fn is_prefix(&self, source: &str, prefix: &str, options: CompareOptions) -> bool;
125
126    /// The position of the first occurrence of `value` in `source` in
127    /// UTF-16 code units, or -1.
128    fn index_of(&self, source: &str, value: &str, options: CompareOptions) -> i32;
129}
130
131/// Implements a set of methods for culture-sensitive string comparisons:
132/// the comparer of a culture.
133#[derive(Clone)]
134pub struct CompareInfo {
135    name: Rc<str>,
136    /// The rules of the culture; the built-in invariant rules if `None`.
137    rules: Option<Rc<dyn ICompareRules>>,
138}
139
140impl CompareInfo {
141    /// The comparer of the invariant culture.
142    pub fn invariant() -> CompareInfo {
143        CompareInfo { name: Rc::from(""), rules: None }
144    }
145
146    /// The name of the culture used for the comparisons.
147    pub fn name(&self) -> &str {
148        &self.name
149    }
150
151    /// Compares two strings using the specified options.
152    pub fn compare(&self, a: &str, b: &str, options: CompareOptions) -> Ordering {
153        match self.culture_rules(options) {
154            Some(rules) => rules.compare(a, b, options),
155            None => {
156                let ignore_case = Self::built_in_ignores_case(options);
157                OrdinalUnits::new(a, ignore_case).cmp(OrdinalUnits::new(b, ignore_case))
158            }
159        }
160    }
161
162    /// Determines whether `source` starts with `prefix` using the specified
163    /// options.
164    pub fn is_prefix(&self, source: &str, prefix: &str, options: CompareOptions) -> bool {
165        match self.culture_rules(options) {
166            Some(rules) => rules.is_prefix(source, prefix, options),
167            None => {
168                let ignore_case = Self::built_in_ignores_case(options);
169                let mut source = OrdinalUnits::new(source, ignore_case);
170                OrdinalUnits::new(prefix, ignore_case).all(|unit| source.next() == Some(unit))
171            }
172        }
173    }
174
175    /// Searches for `value` in `source` using the specified options and
176    /// returns the position of its first occurrence in UTF-16 code units,
177    /// or -1. An empty value is found at 0.
178    pub fn index_of(&self, source: &str, value: &str, options: CompareOptions) -> i32 {
179        match self.culture_rules(options) {
180            Some(rules) => rules.index_of(source, value, options),
181            None => {
182                let ignore_case = Self::built_in_ignores_case(options);
183                let value: Vec<u16> = OrdinalUnits::new(value, ignore_case).collect();
184                if value.is_empty() {
185                    return 0;
186                }
187                let source: Vec<u16> = OrdinalUnits::new(source, ignore_case).collect();
188                source.windows(value.len()).position(|window| window == value.as_slice()).map_or(-1, |index| index as i32)
189            }
190        }
191    }
192
193    /// The rules of the culture for a culture-sensitive comparison; `None`
194    /// for an ordinal comparison and for a culture without rules.
195    fn culture_rules(&self, options: CompareOptions) -> Option<&Rc<dyn ICompareRules>> {
196        if options.intersects(CompareOptions::ORDINAL | CompareOptions::ORDINAL_IGNORE_CASE) {
197            return None;
198        }
199        self.rules.as_ref()
200    }
201
202    /// Whether the built-in comparison for `options` ignores case.
203    fn built_in_ignores_case(options: CompareOptions) -> bool {
204        options.intersects(CompareOptions::IGNORE_CASE | CompareOptions::ORDINAL_IGNORE_CASE)
205    }
206
207    /// The upper-case form of a character under the invariant simple case
208    /// mapping, the mapping of the ordinal comparison that ignores case: one
209    /// character maps to one character (no expansions: `ß` stays `ß`), no
210    /// culture is involved, and the dotless `ı` (U+0131) and the long `ſ`
211    /// (U+017F) keep their identity, as in the reference runtime.
212    pub fn to_upper_ordinal(c: char) -> char {
213        if c.is_ascii() {
214            return c.to_ascii_uppercase();
215        }
216        match c {
217            '\u{0131}' | '\u{017F}' => return c,
218            // The Greek letters with ypogegrammeni: their simple upper-case
219            // forms are the letters with prosgegrammeni, their full ones are
220            // two characters.
221            '\u{1F80}'..='\u{1F87}' | '\u{1F90}'..='\u{1F97}' | '\u{1FA0}'..='\u{1FA7}' => {
222                return char::from_u32(c as u32 + 8).unwrap_or(c);
223            }
224            '\u{1FB3}' => return '\u{1FBC}',
225            '\u{1FC3}' => return '\u{1FCC}',
226            '\u{1FF3}' => return '\u{1FFC}',
227            // Case pairs the reference runtime folds that are newer than the
228            // case tables of some toolchains.
229            '\u{A7CF}' => return '\u{A7CE}',
230            '\u{A7D3}' => return '\u{A7D2}',
231            '\u{A7D5}' => return '\u{A7D4}',
232            _ => {}
233        }
234        let mut upper = c.to_uppercase();
235        match (upper.next(), upper.next()) {
236            (Some(upper), None) => upper,
237            // A character whose only upper-case form is an expansion has no
238            // simple one.
239            _ => c,
240        }
241    }
242}
243
244/// The UTF-16 code units of a text for an ordinal comparison, upper-cased
245/// character by character when the comparison ignores case.
246struct OrdinalUnits<'a> {
247    chars: std::str::Chars<'a>,
248    ignore_case: bool,
249    pending: Option<u16>,
250}
251
252impl<'a> OrdinalUnits<'a> {
253    fn new(text: &'a str, ignore_case: bool) -> Self {
254        Self { chars: text.chars(), ignore_case, pending: None }
255    }
256}
257
258impl Iterator for OrdinalUnits<'_> {
259    type Item = u16;
260
261    fn next(&mut self) -> Option<u16> {
262        if let Some(unit) = self.pending.take() {
263            return Some(unit);
264        }
265        let c = self.chars.next()?;
266        let c = if self.ignore_case { CompareInfo::to_upper_ordinal(c) } else { c };
267        let mut units = [0u16; 2];
268        let units = c.encode_utf16(&mut units);
269        if units.len() == 2 {
270            self.pending = Some(units[1]);
271        }
272        Some(units[0])
273    }
274}
275
276impl CultureInfo {
277    /// The comparer of the culture (C# `CompareInfo`): the rules the culture
278    /// data provider has for the culture or for the nearest of its parents,
279    /// and otherwise the built-in invariant rules.
280    pub fn compare_info(&self) -> CompareInfo {
281        let mut rules = None;
282        if !self.is_invariant() {
283            if let Some(provider) = FerroLocator::current().get_service::<dyn ICultureDataProvider>() {
284                let mut current = self.clone();
285                while rules.is_none() && !current.is_invariant() {
286                    rules = provider.get_compare_rules(current.name());
287                    current = current.parent();
288                }
289            }
290        }
291        CompareInfo { name: Rc::from(self.name()), rules }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn ordinal_compares_code_units() {
301        assert!(StringComparison::Ordinal.equals(Some("name"), Some("name")));
302        assert!(!StringComparison::Ordinal.equals(Some("name"), Some("NAME")));
303        assert!(StringComparison::Ordinal.equals(None, None));
304        assert!(!StringComparison::Ordinal.equals(None, Some("")));
305        assert!(StringComparison::Ordinal.starts_with("name", "na"));
306        assert!(StringComparison::Ordinal.starts_with("name", ""));
307        assert!(!StringComparison::Ordinal.starts_with("name", "NA"));
308        assert_eq!(1, StringComparison::Ordinal.index_of("name", "am"));
309        assert_eq!(0, StringComparison::Ordinal.index_of("name", ""));
310        assert_eq!(-1, StringComparison::Ordinal.index_of("name", "AM"));
311        // A code unit above the surrogates sorts after a surrogate pair.
312        assert_eq!(Ordering::Greater, StringComparison::Ordinal.compare("\u{FF5E}", "\u{10400}"));
313    }
314
315    #[test]
316    fn ordinal_ignore_case_uses_the_simple_upper_case_mapping() {
317        let comparison = StringComparison::OrdinalIgnoreCase;
318        assert!(comparison.equals(Some("name"), Some("NAME")));
319        assert!(comparison.equals(Some("\u{00E9}t\u{00E9}"), Some("\u{00C9}T\u{00C9}")));
320        assert!(comparison.starts_with("name", "NAM"));
321        assert_eq!(1, comparison.index_of("name", "AME"));
322
323        // No expansions: the sharp s is not "SS".
324        assert!(!comparison.equals(Some("stra\u{00DF}e"), Some("STRASSE")));
325        assert!(comparison.equals(Some("\u{00DF}"), Some("\u{00DF}")));
326        assert!(!comparison.contains("stra\u{00DF}e", "SS"));
327
328        // No culture: the Turkish letters keep their identity.
329        assert!(!comparison.equals(Some("\u{0131}"), Some("I")));
330        assert!(!comparison.equals(Some("\u{0131}"), Some("i")));
331        assert!(!comparison.equals(Some("\u{0130}"), Some("i")));
332        assert!(!comparison.equals(Some("\u{0130}"), Some("I")));
333        assert!(comparison.equals(Some("i"), Some("I")));
334
335        // The simple mapping where the full one is an expansion.
336        assert!(comparison.equals(Some("\u{1F80}"), Some("\u{1F88}")));
337        assert!(!comparison.equals(Some("\u{1F80}"), Some("\u{1F08}\u{0399}")));
338    }
339
340    #[test]
341    fn ordinal_ignore_case_handles_surrogate_pairs() {
342        let comparison = StringComparison::OrdinalIgnoreCase;
343        // Two characters that share their high surrogate.
344        assert!(!comparison.equals(Some("\u{1F600}"), Some("\u{1F601}")));
345        assert!(comparison.equals(Some("a\u{1F600}"), Some("A\u{1F600}")));
346        // A pair is folded as the character it encodes.
347        assert!(comparison.equals(Some("\u{10428}"), Some("\u{10400}")));
348        // Positions count code units.
349        assert_eq!(3, comparison.index_of("a\u{1F600}b", "B"));
350        assert_eq!(3, StringComparison::Ordinal.index_of("a\u{1F600}b", "b"));
351    }
352
353    /// Pairs checked against the reference runtime (version 10.0.5), whose
354    /// ordinal comparison that ignores case was also compared with
355    /// [`CompareInfo::to_upper_ordinal`] for every character that has an
356    /// upper-case or a lower-case form there.
357    #[test]
358    fn ordinal_ignore_case_matches_the_reference_runtime_for_edge_characters() {
359        let comparison = StringComparison::OrdinalIgnoreCase;
360        let rows: &[(&str, &str, bool)] = &[
361            // No expansions and no culture.
362            ("\u{00DF}", "SS", false),
363            ("\u{00DF}", "\u{1E9E}", false),
364            ("\u{FB01}", "FI", false),
365            ("\u{0149}", "\u{02BC}N", false),
366            ("\u{0131}", "I", false),
367            ("\u{0131}", "i", false),
368            ("\u{0130}", "i", false),
369            ("\u{0130}", "I", false),
370            ("\u{017F}", "S", false),
371            ("\u{017F}", "s", false),
372            ("\u{212A}", "k", false),
373            ("\u{2126}", "\u{03C9}", false),
374            // The simple upper-case mapping.
375            ("\u{00B5}", "\u{039C}", true),
376            ("\u{00B5}", "\u{03BC}", true),
377            ("\u{03C2}", "\u{03A3}", true),
378            ("\u{03C2}", "\u{03C3}", true),
379            ("\u{01C5}", "\u{01C4}", true),
380            ("\u{01C5}", "\u{01C6}", true),
381            ("\u{1F80}", "\u{1F88}", true),
382            ("\u{1F80}", "\u{1F08}\u{0399}", false),
383            ("\u{10D0}", "\u{1C90}", true),
384            ("\u{1C80}", "\u{0412}", true),
385            ("\u{1C88}", "\u{A64A}", true),
386            ("\u{24D0}", "\u{24B6}", true),
387            ("\u{FF41}", "\u{FF21}", true),
388            ("\u{A7CF}", "\u{A7CE}", true),
389            ("\u{A7D3}", "\u{A7D2}", true),
390            ("\u{A7D5}", "\u{A7D4}", true),
391            // A surrogate pair is folded as the character it encodes.
392            ("\u{10428}", "\u{10400}", true),
393            ("\u{1E922}", "\u{1E900}", true),
394            ("\u{10CC0}", "\u{10C80}", true),
395            ("\u{1F600}", "\u{1F601}", false),
396        ];
397
398        for (a, b, equal) in rows {
399            assert_eq!(*equal, comparison.equals(Some(a), Some(b)), "{a:?} {b:?}");
400            assert_eq!(*equal, comparison.equals(Some(b), Some(a)), "{b:?} {a:?}");
401            assert_eq!(*equal, comparison.starts_with(a, b), "{a:?} starts with {b:?}");
402            let source = format!("a{a}b");
403            assert_eq!(if *equal { 1 } else { -1 }, comparison.index_of(&source, b), "{source:?} index of {b:?}");
404        }
405    }
406
407    #[test]
408    fn culture_comparisons_use_the_built_in_invariant_rules_without_a_provider() {
409        assert!(StringComparison::InvariantCultureIgnoreCase.equals(Some("na"), Some("NA")));
410        assert!(!StringComparison::InvariantCulture.equals(Some("na"), Some("NA")));
411        assert!(StringComparison::CurrentCultureIgnoreCase.starts_with("name", "NAM"));
412        assert!(!StringComparison::CurrentCulture.starts_with("name", "NAM"));
413        assert!(StringComparison::CurrentCultureIgnoreCase.contains("name", "AME"));
414        assert_eq!("", CultureInfo::invariant_culture().compare_info().name());
415    }
416
417    /// Rules that treat every text as equal to every other.
418    struct EverythingIsEqual;
419
420    impl ICompareRules for EverythingIsEqual {
421        fn compare(&self, _a: &str, _b: &str, _options: CompareOptions) -> Ordering {
422            Ordering::Equal
423        }
424
425        fn is_prefix(&self, _source: &str, _prefix: &str, _options: CompareOptions) -> bool {
426            true
427        }
428
429        fn index_of(&self, _source: &str, _value: &str, _options: CompareOptions) -> i32 {
430            0
431        }
432    }
433
434    struct Provider;
435
436    impl ICultureDataProvider for Provider {
437        fn get_compare_rules(&self, culture_name: &str) -> Option<Rc<dyn ICompareRules>> {
438            (culture_name == "xx").then(|| Rc::new(EverythingIsEqual) as Rc<dyn ICompareRules>)
439        }
440    }
441
442    #[test]
443    fn the_rules_of_a_culture_come_from_the_provider() {
444        let scope = FerroLocator::enter_scope();
445        FerroLocator::current_mutable().bind::<dyn ICultureDataProvider>().to_constant(Rc::new(Provider));
446
447        // The rules of the parent culture are used for a culture without.
448        let compare_info = CultureInfo::get_culture_info("xx-YY").compare_info();
449        assert_eq!("xx-YY", compare_info.name());
450        assert_eq!(Ordering::Equal, compare_info.compare("a", "b", CompareOptions::NONE));
451        assert!(compare_info.is_prefix("a", "b", CompareOptions::IGNORE_CASE));
452        // The ordinal comparisons do not reach the rules of the culture.
453        assert_ne!(Ordering::Equal, compare_info.compare("a", "b", CompareOptions::ORDINAL));
454        assert_eq!(-1, compare_info.index_of("a", "b", CompareOptions::ORDINAL_IGNORE_CASE));
455
456        // A culture the provider knows nothing about, and the invariant one.
457        let other = CultureInfo::get_culture_info("zz").compare_info();
458        assert_ne!(Ordering::Equal, other.compare("a", "b", CompareOptions::NONE));
459        assert_ne!(Ordering::Equal, CompareInfo::invariant().compare("a", "b", CompareOptions::NONE));
460
461        scope.dispose();
462    }
463}