1use super::{CultureInfo, ICultureDataProvider};
19use crate::{FerroLocator, LocatorExtensions};
20use bitflags::bitflags;
21use std::cmp::Ordering;
22use std::rc::Rc;
23
24bitflags! {
25 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27 pub struct CompareOptions: i32 {
28 const NONE = 0;
30 const IGNORE_CASE = 0x1;
32 const ORDINAL_IGNORE_CASE = 0x1000_0000;
35 const ORDINAL = 0x4000_0000;
37 }
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[repr(i32)]
44pub enum StringComparison {
45 CurrentCulture = 0,
48 CurrentCultureIgnoreCase = 1,
51 InvariantCulture = 2,
54 InvariantCultureIgnoreCase = 3,
57 Ordinal = 4,
59 OrdinalIgnoreCase = 5,
62}
63
64impl StringComparison {
65 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 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 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 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 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 pub fn contains(self, source: &str, value: &str) -> bool {
112 self.index_of(source, value) >= 0
113 }
114}
115
116pub trait ICompareRules: 'static {
120 fn compare(&self, a: &str, b: &str, options: CompareOptions) -> Ordering;
122
123 fn is_prefix(&self, source: &str, prefix: &str, options: CompareOptions) -> bool;
125
126 fn index_of(&self, source: &str, value: &str, options: CompareOptions) -> i32;
129}
130
131#[derive(Clone)]
134pub struct CompareInfo {
135 name: Rc<str>,
136 rules: Option<Rc<dyn ICompareRules>>,
138}
139
140impl CompareInfo {
141 pub fn invariant() -> CompareInfo {
143 CompareInfo { name: Rc::from(""), rules: None }
144 }
145
146 pub fn name(&self) -> &str {
148 &self.name
149 }
150
151 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 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 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 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 fn built_in_ignores_case(options: CompareOptions) -> bool {
204 options.intersects(CompareOptions::IGNORE_CASE | CompareOptions::ORDINAL_IGNORE_CASE)
205 }
206
207 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 '\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 '\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 _ => c,
240 }
241 }
242}
243
244struct 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 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 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 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 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 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 assert!(!comparison.equals(Some("\u{1F600}"), Some("\u{1F601}")));
345 assert!(comparison.equals(Some("a\u{1F600}"), Some("A\u{1F600}")));
346 assert!(comparison.equals(Some("\u{10428}"), Some("\u{10400}")));
348 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 #[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 ("\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 ("\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 ("\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 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 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 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 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}